├── .vscode └── settings.json ├── LICENSE ├── README.md ├── backend ├── .gitignore ├── codegen.yml ├── package.json ├── prisma │ ├── datamodel.prisma │ ├── docker-compose.yml │ ├── prisma.yml │ └── seed.graphql ├── src │ ├── generated │ │ ├── prisma-client │ │ │ ├── index.ts │ │ │ └── prisma-schema.ts │ │ └── types.ts │ ├── index.ts │ ├── resolvers │ │ ├── index.ts │ │ ├── mutation │ │ │ ├── index.ts │ │ │ └── userMutation.ts │ │ └── query │ │ │ ├── index.ts │ │ │ └── userQuery.ts │ ├── schema.graphql │ └── utils.ts └── tsconfig.json ├── frontend ├── .gitignore ├── package-lock.json ├── package.json ├── public │ ├── favicon.ico │ ├── index.html │ ├── manifest.json │ └── robots.txt ├── src │ ├── assets │ │ └── .keep │ ├── components │ │ └── UserList │ │ │ ├── User │ │ │ ├── User.tsx │ │ │ └── index.ts │ │ │ ├── UserList.tsx │ │ │ ├── UserListContainer.tsx │ │ │ └── index.ts │ ├── graphql │ │ ├── fragments │ │ │ └── user.ts │ │ ├── mutation │ │ │ └── user.ts │ │ └── query │ │ │ └── user.ts │ ├── index.css │ ├── index.tsx │ ├── pages │ │ └── App │ │ │ ├── App.css │ │ │ ├── App.test.tsx │ │ │ ├── App.tsx │ │ │ └── index.ts │ ├── react-app-env.d.ts │ ├── serviceWorker.ts │ └── types.ts └── tsconfig.json └── package-lock.json /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true 3 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Aden Herold 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-graphql-typescript-boilerplate 2 | 3 | ## **What is it** 4 | 5 | This repository is intended to be used as a boilerplate for any of my projects utilizing a React + Apollo frontend, and a Node/GraphQL backend. Both the frontend and backend projects are bootstrapped with, and take advantage of, Typescript. You are free to use this boilerplate for your own projects if you desire. 6 | 7 | ## **Prerequisites** 8 | 9 | In order to use this boilerplate, you must install `docker` and `docker-compose` on your system. 10 | 11 | ## **Technology** 12 | 13 | ### **Backend** 14 | 15 | I've configured the GraphQL backend to be scalable, to prevent query and mutation resolvers from becoming overwhelming as more and more types are added the the GraphQL schema. 16 | 17 | I haven't added any subscription resolvers in the boilerplate, but if you would like them to also be scalable, consider following the convention I've been using. 18 | 19 | In `package.json`, I've included a script named _codegen_, which utilizes `graphql-codegen` to generate Typescript types for your GraphQL Schema, and output them to `backend/src/generated/types.ts` and `frontend/src/types.ts`. This script is automatically executed before the Node server runs, but you can manually execute it via `npm run codegen` 20 | 21 | The backend is comprised of the following key technologies: 22 | 23 | - **GraphQL-Yoga**: A fully featured GraphQL server, with most useful features available right out of the box, including subscriptions, GraphQL Playground, etc. I've configured the server to support Typescript. [https://github.com/prisma-labs/graphql-yoga](https://github.com/prisma-labs/graphql-yoga) 24 | 25 | - **Prisma**: A set of database tools, including an ORM, which is utilized for the GraphQL server. In this boilerplate, I'm using the MongoDB connector, but you can feel free to use any of the other [supported connectors](https://www.prisma.io/features/databases). If you are using a different connector, then you will need to edit `prisma/docker-compose.yml`. [https://github.com/prisma/prisma](https://github.com/prisma/prisma) 26 | 27 | ### **Frontend** 28 | 29 | I've also configured the React frontend project to be scalable, using a components & pages directory structure. I've added some basic container + presentational components to demonstrate the connectivity with the GraphQL server, but feel free to remove them. 30 | 31 | The frontend is comprised of the following key technologies: 32 | 33 | - **React**: It should go without saying, but React is a powerful library for creating user interfaces. I've configured the project to support Typescript and CSS Modules, via `create-react-app`, but feel free to eject and make any configuration changes of your own. [https://github.com/facebook/react](https://github.com/facebook/react) 34 | 35 | - **Apollo-Client**: A fully featured GraphQL client to interface with the backend GraphQL server. I specifically utilize [apollo-boost](https://github.com/apollographql/apollo-client/tree/master/packages/apollo-boost), which is a quick and easy, out of the box configuration for the Apollo Client. [https://github.com/apollographql/apollo-client](https://github.com/apollographql/apollo-client) 36 | 37 | - **React-Apollo**: Provides a Higher Order Component (HOC) to provide the Apollo Client instance to all wrapped components, and allow them to be enhanced with Apollo functionalities. `react-apollo` can essentially replace redux in a way, as it can take full responsibility of fetching and caching data as well as updating the UI, which is why Redux is not included in this boilerplate. If you feel that `react-apollo` is simply not enough to manage your state, then consider the React Context API before needlessly adding Redux boilerplate. [https://github.com/apollographql/react-apollo](https://github.com/apollographql/react-apollo) 38 | 39 | ## **How to build** 40 | 41 | ### **Backend** 42 | 43 | - Install dependencies via `npm install`. 44 | - Execute `docker-compose up -d` from within `/prisma`. 45 | - Execute `npm run prisma-deploy` to deploy your Prisma schema + generate the prisma client. 46 | - Execute `npm start` to start the server via `nodemon`. 47 | 48 | ### **Frontend** 49 | 50 | - Install dependencies via `npm install`. 51 | - Execute `npm start` to start the Webpack dev server. 52 | -------------------------------------------------------------------------------- /backend/.gitignore: -------------------------------------------------------------------------------- 1 | .env* 2 | dist 3 | package-lock.json 4 | node_modules 5 | .idea 6 | .vscode 7 | *.log -------------------------------------------------------------------------------- /backend/codegen.yml: -------------------------------------------------------------------------------- 1 | overwrite: true 2 | schema: "src/schema.graphql" 3 | generates: 4 | src/generated/types.ts: 5 | plugins: 6 | - typescript 7 | ../frontend/src/types.ts: 8 | plugins: 9 | - typescript 10 | -------------------------------------------------------------------------------- /backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "backend", 3 | "scripts": { 4 | "start": "nodemon -e json,js,ts,graphql,prisma --ignore 'src/**/*.spec.ts' --exec 'npm run codegen && ts-node' src/index.ts", 5 | "prisma-deploy": "prisma deploy -p prisma/prisma.yml", 6 | "prisma-delete": "prisma delete -p prisma/prisma.yml", 7 | "codegen": "graphql-codegen --config codegen.yml" 8 | }, 9 | "dependencies": { 10 | "graphql-yoga": "1.18.3", 11 | "prisma-client-lib": "1.34.10" 12 | }, 13 | "devDependencies": { 14 | "@graphql-codegen/cli": "^1.9.1", 15 | "@graphql-codegen/typescript": "^1.9.1", 16 | "nodemon": "^2.0.1", 17 | "prisma": "1.34.10", 18 | "ts-node": "7.0.1", 19 | "typescript": "3.2.2" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /backend/prisma/datamodel.prisma: -------------------------------------------------------------------------------- 1 | type User { 2 | id: ID! @id 3 | username: String! @unique 4 | email: String! @unique 5 | name: String! 6 | createdAt: DateTime! @createdAt 7 | updatedAt: DateTime! @updatedAt 8 | } -------------------------------------------------------------------------------- /backend/prisma/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | prisma: 4 | image: prismagraphql/prisma:1.34 5 | restart: always 6 | ports: 7 | - "4466:4466" 8 | environment: 9 | PRISMA_CONFIG: | 10 | port: 4466 11 | # uncomment the next line and provide the env var PRISMA_MANAGEMENT_API_SECRET=my-secret to activate cluster security 12 | # managementApiSecret: my-secret 13 | databases: 14 | default: 15 | connector: mongo 16 | uri: 'mongodb://prisma:prisma@mongo' 17 | mongo: 18 | image: mongo:3.6 19 | restart: always 20 | # Uncomment the next two lines to connect to your your database from outside the Docker environment, e.g. using a database GUI like Compass 21 | # ports: 22 | # - "27017:27017" 23 | environment: 24 | MONGO_INITDB_ROOT_USERNAME: prisma 25 | MONGO_INITDB_ROOT_PASSWORD: prisma 26 | ports: 27 | - "27017:27017" 28 | volumes: 29 | - mongo:/var/lib/mongo 30 | volumes: 31 | mongo: 32 | -------------------------------------------------------------------------------- /backend/prisma/prisma.yml: -------------------------------------------------------------------------------- 1 | endpoint: http://localhost:4466/ 2 | datamodel: datamodel.prisma 3 | databaseType: document 4 | 5 | generate: 6 | - generator: typescript-client 7 | output: ../src/generated/prisma-client/ 8 | 9 | seed: 10 | import: seed.graphql 11 | 12 | hooks: 13 | post-deploy: 14 | - prisma generate 15 | -------------------------------------------------------------------------------- /backend/prisma/seed.graphql: -------------------------------------------------------------------------------- 1 | mutation { 2 | user1: createPost( 3 | data: { 4 | email: "joe.smith@prisma.com" 5 | username: "joe.smith" 6 | name: "Joe Smith" 7 | } 8 | ) { 9 | id 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /backend/src/generated/prisma-client/index.ts: -------------------------------------------------------------------------------- 1 | // Code generated by Prisma (prisma@1.34.10). DO NOT EDIT. 2 | // Please don't change this file manually but run `prisma generate` to update it. 3 | // For more information, please read the docs: https://www.prisma.io/docs/prisma-client/ 4 | 5 | import { DocumentNode } from "graphql"; 6 | import { 7 | makePrismaClientClass, 8 | BaseClientOptions, 9 | Model 10 | } from "prisma-client-lib"; 11 | import { typeDefs } from "./prisma-schema"; 12 | 13 | export type AtLeastOne }> = Partial & 14 | U[keyof U]; 15 | 16 | export type Maybe = T | undefined | null; 17 | 18 | export interface Exists { 19 | user: (where?: UserWhereInput) => Promise; 20 | } 21 | 22 | export interface Node {} 23 | 24 | export type FragmentableArray = Promise> & Fragmentable; 25 | 26 | export interface Fragmentable { 27 | $fragment(fragment: string | DocumentNode): Promise; 28 | } 29 | 30 | export interface Prisma { 31 | $exists: Exists; 32 | $graphql: ( 33 | query: string, 34 | variables?: { [key: string]: any } 35 | ) => Promise; 36 | 37 | /** 38 | * Queries 39 | */ 40 | 41 | user: (where: UserWhereUniqueInput) => UserNullablePromise; 42 | users: (args?: { 43 | where?: UserWhereInput; 44 | orderBy?: UserOrderByInput; 45 | skip?: Int; 46 | after?: String; 47 | before?: String; 48 | first?: Int; 49 | last?: Int; 50 | }) => FragmentableArray; 51 | usersConnection: (args?: { 52 | where?: UserWhereInput; 53 | orderBy?: UserOrderByInput; 54 | skip?: Int; 55 | after?: String; 56 | before?: String; 57 | first?: Int; 58 | last?: Int; 59 | }) => UserConnectionPromise; 60 | node: (args: { id: ID_Output }) => Node; 61 | 62 | /** 63 | * Mutations 64 | */ 65 | 66 | createUser: (data: UserCreateInput) => UserPromise; 67 | updateUser: (args: { 68 | data: UserUpdateInput; 69 | where: UserWhereUniqueInput; 70 | }) => UserPromise; 71 | updateManyUsers: (args: { 72 | data: UserUpdateManyMutationInput; 73 | where?: UserWhereInput; 74 | }) => BatchPayloadPromise; 75 | upsertUser: (args: { 76 | where: UserWhereUniqueInput; 77 | create: UserCreateInput; 78 | update: UserUpdateInput; 79 | }) => UserPromise; 80 | deleteUser: (where: UserWhereUniqueInput) => UserPromise; 81 | deleteManyUsers: (where?: UserWhereInput) => BatchPayloadPromise; 82 | 83 | /** 84 | * Subscriptions 85 | */ 86 | 87 | $subscribe: Subscription; 88 | } 89 | 90 | export interface Subscription { 91 | user: ( 92 | where?: UserSubscriptionWhereInput 93 | ) => UserSubscriptionPayloadSubscription; 94 | } 95 | 96 | export interface ClientConstructor { 97 | new (options?: BaseClientOptions): T; 98 | } 99 | 100 | /** 101 | * Types 102 | */ 103 | 104 | export type UserOrderByInput = 105 | | "id_ASC" 106 | | "id_DESC" 107 | | "username_ASC" 108 | | "username_DESC" 109 | | "email_ASC" 110 | | "email_DESC" 111 | | "name_ASC" 112 | | "name_DESC" 113 | | "createdAt_ASC" 114 | | "createdAt_DESC" 115 | | "updatedAt_ASC" 116 | | "updatedAt_DESC"; 117 | 118 | export type MutationType = "CREATED" | "UPDATED" | "DELETED"; 119 | 120 | export type UserWhereUniqueInput = AtLeastOne<{ 121 | id: Maybe; 122 | username?: Maybe; 123 | email?: Maybe; 124 | }>; 125 | 126 | export interface UserWhereInput { 127 | id?: Maybe; 128 | id_not?: Maybe; 129 | id_in?: Maybe; 130 | id_not_in?: Maybe; 131 | id_lt?: Maybe; 132 | id_lte?: Maybe; 133 | id_gt?: Maybe; 134 | id_gte?: Maybe; 135 | id_contains?: Maybe; 136 | id_not_contains?: Maybe; 137 | id_starts_with?: Maybe; 138 | id_not_starts_with?: Maybe; 139 | id_ends_with?: Maybe; 140 | id_not_ends_with?: Maybe; 141 | username?: Maybe; 142 | username_not?: Maybe; 143 | username_in?: Maybe; 144 | username_not_in?: Maybe; 145 | username_lt?: Maybe; 146 | username_lte?: Maybe; 147 | username_gt?: Maybe; 148 | username_gte?: Maybe; 149 | username_contains?: Maybe; 150 | username_not_contains?: Maybe; 151 | username_starts_with?: Maybe; 152 | username_not_starts_with?: Maybe; 153 | username_ends_with?: Maybe; 154 | username_not_ends_with?: Maybe; 155 | email?: Maybe; 156 | email_not?: Maybe; 157 | email_in?: Maybe; 158 | email_not_in?: Maybe; 159 | email_lt?: Maybe; 160 | email_lte?: Maybe; 161 | email_gt?: Maybe; 162 | email_gte?: Maybe; 163 | email_contains?: Maybe; 164 | email_not_contains?: Maybe; 165 | email_starts_with?: Maybe; 166 | email_not_starts_with?: Maybe; 167 | email_ends_with?: Maybe; 168 | email_not_ends_with?: Maybe; 169 | name?: Maybe; 170 | name_not?: Maybe; 171 | name_in?: Maybe; 172 | name_not_in?: Maybe; 173 | name_lt?: Maybe; 174 | name_lte?: Maybe; 175 | name_gt?: Maybe; 176 | name_gte?: Maybe; 177 | name_contains?: Maybe; 178 | name_not_contains?: Maybe; 179 | name_starts_with?: Maybe; 180 | name_not_starts_with?: Maybe; 181 | name_ends_with?: Maybe; 182 | name_not_ends_with?: Maybe; 183 | createdAt?: Maybe; 184 | createdAt_not?: Maybe; 185 | createdAt_in?: Maybe; 186 | createdAt_not_in?: Maybe; 187 | createdAt_lt?: Maybe; 188 | createdAt_lte?: Maybe; 189 | createdAt_gt?: Maybe; 190 | createdAt_gte?: Maybe; 191 | updatedAt?: Maybe; 192 | updatedAt_not?: Maybe; 193 | updatedAt_in?: Maybe; 194 | updatedAt_not_in?: Maybe; 195 | updatedAt_lt?: Maybe; 196 | updatedAt_lte?: Maybe; 197 | updatedAt_gt?: Maybe; 198 | updatedAt_gte?: Maybe; 199 | AND?: Maybe; 200 | } 201 | 202 | export interface UserCreateInput { 203 | id?: Maybe; 204 | username: String; 205 | email: String; 206 | name: String; 207 | } 208 | 209 | export interface UserUpdateInput { 210 | username?: Maybe; 211 | email?: Maybe; 212 | name?: Maybe; 213 | } 214 | 215 | export interface UserUpdateManyMutationInput { 216 | username?: Maybe; 217 | email?: Maybe; 218 | name?: Maybe; 219 | } 220 | 221 | export interface UserSubscriptionWhereInput { 222 | mutation_in?: Maybe; 223 | updatedFields_contains?: Maybe; 224 | updatedFields_contains_every?: Maybe; 225 | updatedFields_contains_some?: Maybe; 226 | node?: Maybe; 227 | AND?: Maybe; 228 | } 229 | 230 | export interface NodeNode { 231 | id: ID_Output; 232 | } 233 | 234 | export interface User { 235 | id: ID_Output; 236 | username: String; 237 | email: String; 238 | name: String; 239 | createdAt: DateTimeOutput; 240 | updatedAt: DateTimeOutput; 241 | } 242 | 243 | export interface UserPromise extends Promise, Fragmentable { 244 | id: () => Promise; 245 | username: () => Promise; 246 | email: () => Promise; 247 | name: () => Promise; 248 | createdAt: () => Promise; 249 | updatedAt: () => Promise; 250 | } 251 | 252 | export interface UserSubscription 253 | extends Promise>, 254 | Fragmentable { 255 | id: () => Promise>; 256 | username: () => Promise>; 257 | email: () => Promise>; 258 | name: () => Promise>; 259 | createdAt: () => Promise>; 260 | updatedAt: () => Promise>; 261 | } 262 | 263 | export interface UserNullablePromise 264 | extends Promise, 265 | Fragmentable { 266 | id: () => Promise; 267 | username: () => Promise; 268 | email: () => Promise; 269 | name: () => Promise; 270 | createdAt: () => Promise; 271 | updatedAt: () => Promise; 272 | } 273 | 274 | export interface UserConnection { 275 | pageInfo: PageInfo; 276 | edges: UserEdge[]; 277 | } 278 | 279 | export interface UserConnectionPromise 280 | extends Promise, 281 | Fragmentable { 282 | pageInfo: () => T; 283 | edges: >() => T; 284 | aggregate: () => T; 285 | } 286 | 287 | export interface UserConnectionSubscription 288 | extends Promise>, 289 | Fragmentable { 290 | pageInfo: () => T; 291 | edges: >>() => T; 292 | aggregate: () => T; 293 | } 294 | 295 | export interface PageInfo { 296 | hasNextPage: Boolean; 297 | hasPreviousPage: Boolean; 298 | startCursor?: String; 299 | endCursor?: String; 300 | } 301 | 302 | export interface PageInfoPromise extends Promise, Fragmentable { 303 | hasNextPage: () => Promise; 304 | hasPreviousPage: () => Promise; 305 | startCursor: () => Promise; 306 | endCursor: () => Promise; 307 | } 308 | 309 | export interface PageInfoSubscription 310 | extends Promise>, 311 | Fragmentable { 312 | hasNextPage: () => Promise>; 313 | hasPreviousPage: () => Promise>; 314 | startCursor: () => Promise>; 315 | endCursor: () => Promise>; 316 | } 317 | 318 | export interface UserEdge { 319 | node: User; 320 | cursor: String; 321 | } 322 | 323 | export interface UserEdgePromise extends Promise, Fragmentable { 324 | node: () => T; 325 | cursor: () => Promise; 326 | } 327 | 328 | export interface UserEdgeSubscription 329 | extends Promise>, 330 | Fragmentable { 331 | node: () => T; 332 | cursor: () => Promise>; 333 | } 334 | 335 | export interface AggregateUser { 336 | count: Int; 337 | } 338 | 339 | export interface AggregateUserPromise 340 | extends Promise, 341 | Fragmentable { 342 | count: () => Promise; 343 | } 344 | 345 | export interface AggregateUserSubscription 346 | extends Promise>, 347 | Fragmentable { 348 | count: () => Promise>; 349 | } 350 | 351 | export interface BatchPayload { 352 | count: Long; 353 | } 354 | 355 | export interface BatchPayloadPromise 356 | extends Promise, 357 | Fragmentable { 358 | count: () => Promise; 359 | } 360 | 361 | export interface BatchPayloadSubscription 362 | extends Promise>, 363 | Fragmentable { 364 | count: () => Promise>; 365 | } 366 | 367 | export interface UserSubscriptionPayload { 368 | mutation: MutationType; 369 | node: User; 370 | updatedFields: String[]; 371 | previousValues: UserPreviousValues; 372 | } 373 | 374 | export interface UserSubscriptionPayloadPromise 375 | extends Promise, 376 | Fragmentable { 377 | mutation: () => Promise; 378 | node: () => T; 379 | updatedFields: () => Promise; 380 | previousValues: () => T; 381 | } 382 | 383 | export interface UserSubscriptionPayloadSubscription 384 | extends Promise>, 385 | Fragmentable { 386 | mutation: () => Promise>; 387 | node: () => T; 388 | updatedFields: () => Promise>; 389 | previousValues: () => T; 390 | } 391 | 392 | export interface UserPreviousValues { 393 | id: ID_Output; 394 | username: String; 395 | email: String; 396 | name: String; 397 | createdAt: DateTimeOutput; 398 | updatedAt: DateTimeOutput; 399 | } 400 | 401 | export interface UserPreviousValuesPromise 402 | extends Promise, 403 | Fragmentable { 404 | id: () => Promise; 405 | username: () => Promise; 406 | email: () => Promise; 407 | name: () => Promise; 408 | createdAt: () => Promise; 409 | updatedAt: () => Promise; 410 | } 411 | 412 | export interface UserPreviousValuesSubscription 413 | extends Promise>, 414 | Fragmentable { 415 | id: () => Promise>; 416 | username: () => Promise>; 417 | email: () => Promise>; 418 | name: () => Promise>; 419 | createdAt: () => Promise>; 420 | updatedAt: () => Promise>; 421 | } 422 | 423 | /* 424 | The `ID` scalar type represents a unique identifier, often used to refetch an object or as key for a cache. The ID type appears in a JSON response as a String; however, it is not intended to be human-readable. When expected as an input type, any string (such as `"4"`) or integer (such as `4`) input value will be accepted as an ID. 425 | */ 426 | export type ID_Input = string | number; 427 | export type ID_Output = string; 428 | 429 | /* 430 | The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. 431 | */ 432 | export type String = string; 433 | 434 | /* 435 | DateTime scalar input type, allowing Date 436 | */ 437 | export type DateTimeInput = Date | string; 438 | 439 | /* 440 | DateTime scalar output type, which is always a string 441 | */ 442 | export type DateTimeOutput = string; 443 | 444 | /* 445 | The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1. 446 | */ 447 | export type Int = number; 448 | 449 | /* 450 | The `Boolean` scalar type represents `true` or `false`. 451 | */ 452 | export type Boolean = boolean; 453 | 454 | export type Long = string; 455 | 456 | /** 457 | * Model Metadata 458 | */ 459 | 460 | export const models: Model[] = [ 461 | { 462 | name: "User", 463 | embedded: false 464 | } 465 | ]; 466 | 467 | /** 468 | * Type Defs 469 | */ 470 | 471 | export const Prisma = makePrismaClientClass>({ 472 | typeDefs, 473 | models, 474 | endpoint: `http://localhost:4466/` 475 | }); 476 | export const prisma = new Prisma(); 477 | -------------------------------------------------------------------------------- /backend/src/generated/prisma-client/prisma-schema.ts: -------------------------------------------------------------------------------- 1 | // Code generated by Prisma (prisma@1.34.10). DO NOT EDIT. 2 | // Please don't change this file manually but run `prisma generate` to update it. 3 | // For more information, please read the docs: https://www.prisma.io/docs/prisma-client/ 4 | 5 | export const typeDefs = /* GraphQL */ `type AggregateUser { 6 | count: Int! 7 | } 8 | 9 | type BatchPayload { 10 | count: Long! 11 | } 12 | 13 | scalar DateTime 14 | 15 | scalar Long 16 | 17 | type Mutation { 18 | createUser(data: UserCreateInput!): User! 19 | updateUser(data: UserUpdateInput!, where: UserWhereUniqueInput!): User 20 | updateManyUsers(data: UserUpdateManyMutationInput!, where: UserWhereInput): BatchPayload! 21 | upsertUser(where: UserWhereUniqueInput!, create: UserCreateInput!, update: UserUpdateInput!): User! 22 | deleteUser(where: UserWhereUniqueInput!): User 23 | deleteManyUsers(where: UserWhereInput): BatchPayload! 24 | } 25 | 26 | enum MutationType { 27 | CREATED 28 | UPDATED 29 | DELETED 30 | } 31 | 32 | interface Node { 33 | id: ID! 34 | } 35 | 36 | type PageInfo { 37 | hasNextPage: Boolean! 38 | hasPreviousPage: Boolean! 39 | startCursor: String 40 | endCursor: String 41 | } 42 | 43 | type Query { 44 | user(where: UserWhereUniqueInput!): User 45 | users(where: UserWhereInput, orderBy: UserOrderByInput, skip: Int, after: String, before: String, first: Int, last: Int): [User]! 46 | usersConnection(where: UserWhereInput, orderBy: UserOrderByInput, skip: Int, after: String, before: String, first: Int, last: Int): UserConnection! 47 | node(id: ID!): Node 48 | } 49 | 50 | type Subscription { 51 | user(where: UserSubscriptionWhereInput): UserSubscriptionPayload 52 | } 53 | 54 | type User { 55 | id: ID! 56 | username: String! 57 | email: String! 58 | name: String! 59 | createdAt: DateTime! 60 | updatedAt: DateTime! 61 | } 62 | 63 | type UserConnection { 64 | pageInfo: PageInfo! 65 | edges: [UserEdge]! 66 | aggregate: AggregateUser! 67 | } 68 | 69 | input UserCreateInput { 70 | id: ID 71 | username: String! 72 | email: String! 73 | name: String! 74 | } 75 | 76 | type UserEdge { 77 | node: User! 78 | cursor: String! 79 | } 80 | 81 | enum UserOrderByInput { 82 | id_ASC 83 | id_DESC 84 | username_ASC 85 | username_DESC 86 | email_ASC 87 | email_DESC 88 | name_ASC 89 | name_DESC 90 | createdAt_ASC 91 | createdAt_DESC 92 | updatedAt_ASC 93 | updatedAt_DESC 94 | } 95 | 96 | type UserPreviousValues { 97 | id: ID! 98 | username: String! 99 | email: String! 100 | name: String! 101 | createdAt: DateTime! 102 | updatedAt: DateTime! 103 | } 104 | 105 | type UserSubscriptionPayload { 106 | mutation: MutationType! 107 | node: User 108 | updatedFields: [String!] 109 | previousValues: UserPreviousValues 110 | } 111 | 112 | input UserSubscriptionWhereInput { 113 | mutation_in: [MutationType!] 114 | updatedFields_contains: String 115 | updatedFields_contains_every: [String!] 116 | updatedFields_contains_some: [String!] 117 | node: UserWhereInput 118 | AND: [UserSubscriptionWhereInput!] 119 | } 120 | 121 | input UserUpdateInput { 122 | username: String 123 | email: String 124 | name: String 125 | } 126 | 127 | input UserUpdateManyMutationInput { 128 | username: String 129 | email: String 130 | name: String 131 | } 132 | 133 | input UserWhereInput { 134 | id: ID 135 | id_not: ID 136 | id_in: [ID!] 137 | id_not_in: [ID!] 138 | id_lt: ID 139 | id_lte: ID 140 | id_gt: ID 141 | id_gte: ID 142 | id_contains: ID 143 | id_not_contains: ID 144 | id_starts_with: ID 145 | id_not_starts_with: ID 146 | id_ends_with: ID 147 | id_not_ends_with: ID 148 | username: String 149 | username_not: String 150 | username_in: [String!] 151 | username_not_in: [String!] 152 | username_lt: String 153 | username_lte: String 154 | username_gt: String 155 | username_gte: String 156 | username_contains: String 157 | username_not_contains: String 158 | username_starts_with: String 159 | username_not_starts_with: String 160 | username_ends_with: String 161 | username_not_ends_with: String 162 | email: String 163 | email_not: String 164 | email_in: [String!] 165 | email_not_in: [String!] 166 | email_lt: String 167 | email_lte: String 168 | email_gt: String 169 | email_gte: String 170 | email_contains: String 171 | email_not_contains: String 172 | email_starts_with: String 173 | email_not_starts_with: String 174 | email_ends_with: String 175 | email_not_ends_with: String 176 | name: String 177 | name_not: String 178 | name_in: [String!] 179 | name_not_in: [String!] 180 | name_lt: String 181 | name_lte: String 182 | name_gt: String 183 | name_gte: String 184 | name_contains: String 185 | name_not_contains: String 186 | name_starts_with: String 187 | name_not_starts_with: String 188 | name_ends_with: String 189 | name_not_ends_with: String 190 | createdAt: DateTime 191 | createdAt_not: DateTime 192 | createdAt_in: [DateTime!] 193 | createdAt_not_in: [DateTime!] 194 | createdAt_lt: DateTime 195 | createdAt_lte: DateTime 196 | createdAt_gt: DateTime 197 | createdAt_gte: DateTime 198 | updatedAt: DateTime 199 | updatedAt_not: DateTime 200 | updatedAt_in: [DateTime!] 201 | updatedAt_not_in: [DateTime!] 202 | updatedAt_lt: DateTime 203 | updatedAt_lte: DateTime 204 | updatedAt_gt: DateTime 205 | updatedAt_gte: DateTime 206 | AND: [UserWhereInput!] 207 | } 208 | 209 | input UserWhereUniqueInput { 210 | id: ID 211 | username: String 212 | email: String 213 | } 214 | ` -------------------------------------------------------------------------------- /backend/src/generated/types.ts: -------------------------------------------------------------------------------- 1 | export type Maybe = T | null; 2 | /** All built-in and custom scalars, mapped to their actual values */ 3 | export type Scalars = { 4 | ID: string, 5 | String: string, 6 | Boolean: boolean, 7 | Int: number, 8 | Float: number, 9 | }; 10 | 11 | export type CreateUserInput = { 12 | username: Scalars['String'], 13 | email: Scalars['String'], 14 | name: Scalars['String'], 15 | }; 16 | 17 | export type Mutation = { 18 | __typename?: 'Mutation', 19 | createUser?: Maybe, 20 | updateUser?: Maybe, 21 | deleteUser?: Maybe, 22 | }; 23 | 24 | 25 | export type MutationCreateUserArgs = { 26 | data?: Maybe 27 | }; 28 | 29 | 30 | export type MutationUpdateUserArgs = { 31 | id?: Maybe, 32 | data?: Maybe 33 | }; 34 | 35 | 36 | export type MutationDeleteUserArgs = { 37 | id?: Maybe 38 | }; 39 | 40 | export type Query = { 41 | __typename?: 'Query', 42 | user?: Maybe, 43 | users: Array, 44 | }; 45 | 46 | 47 | export type QueryUserArgs = { 48 | id: Scalars['ID'] 49 | }; 50 | 51 | export type UpdateUserInput = { 52 | username?: Maybe, 53 | email?: Maybe, 54 | name?: Maybe, 55 | }; 56 | 57 | export type User = { 58 | __typename?: 'User', 59 | id: Scalars['ID'], 60 | username: Scalars['String'], 61 | email: Scalars['String'], 62 | name: Scalars['String'], 63 | }; 64 | -------------------------------------------------------------------------------- /backend/src/index.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLServer } from "graphql-yoga"; 2 | import { prisma } from "./generated/prisma-client"; 3 | import resolvers from "./resolvers"; 4 | 5 | const server = new GraphQLServer({ 6 | typeDefs: "./src/schema.graphql", 7 | resolvers, 8 | context: { prisma } 9 | }); 10 | 11 | server.start(() => console.log("Server is running on http://localhost:4000")); 12 | -------------------------------------------------------------------------------- /backend/src/resolvers/index.ts: -------------------------------------------------------------------------------- 1 | import Query from "./query"; 2 | import Mutation from "./mutation"; 3 | 4 | const resolvers = { 5 | Query, 6 | Mutation 7 | }; 8 | 9 | export default resolvers; 10 | -------------------------------------------------------------------------------- /backend/src/resolvers/mutation/index.ts: -------------------------------------------------------------------------------- 1 | import userMutation from "./userMutation"; 2 | 3 | const Mutation = { 4 | ...userMutation 5 | }; 6 | 7 | export default Mutation; 8 | -------------------------------------------------------------------------------- /backend/src/resolvers/mutation/userMutation.ts: -------------------------------------------------------------------------------- 1 | import { Context } from "../../utils"; 2 | import { 3 | MutationCreateUserArgs, 4 | MutationUpdateUserArgs, 5 | MutationDeleteUserArgs 6 | } from "../../generated/types"; 7 | 8 | const userMutation = { 9 | createUser(parent, { data }: MutationCreateUserArgs, { prisma }: Context) { 10 | return prisma.createUser(data); 11 | }, 12 | updateUser( 13 | parent, 14 | { id, data }: MutationUpdateUserArgs, 15 | { prisma }: Context 16 | ) { 17 | return prisma.updateUser({ where: { id }, data }); 18 | }, 19 | deleteUser(parent, { id }: MutationDeleteUserArgs, { prisma }: Context) { 20 | return prisma.deleteUser({ id }); 21 | } 22 | }; 23 | 24 | export default userMutation; 25 | -------------------------------------------------------------------------------- /backend/src/resolvers/query/index.ts: -------------------------------------------------------------------------------- 1 | import userQuery from "./userQuery"; 2 | 3 | const Query = { 4 | ...userQuery 5 | }; 6 | 7 | export default Query; 8 | -------------------------------------------------------------------------------- /backend/src/resolvers/query/userQuery.ts: -------------------------------------------------------------------------------- 1 | import { Context } from "../../utils"; 2 | import { QueryUserArgs } from "../../generated/types"; 3 | 4 | const userQuery = { 5 | users(parent, args, context: Context) { 6 | return context.prisma.users(); 7 | }, 8 | user(parent, { id }: QueryUserArgs, context: Context) { 9 | return context.prisma.user({ id }); 10 | } 11 | }; 12 | 13 | export default userQuery; 14 | -------------------------------------------------------------------------------- /backend/src/schema.graphql: -------------------------------------------------------------------------------- 1 | type Query { 2 | user(id: ID!): User 3 | users: [User!]! 4 | } 5 | 6 | type Mutation { 7 | createUser(data: CreateUserInput): User 8 | updateUser(id: ID, data: UpdateUserInput): User 9 | deleteUser(id: ID): User 10 | } 11 | 12 | type User { 13 | id: ID! 14 | username: String! 15 | email: String! 16 | name: String! 17 | } 18 | 19 | input CreateUserInput { 20 | username: String! 21 | email: String! 22 | name: String! 23 | } 24 | 25 | input UpdateUserInput { 26 | username: String 27 | email: String 28 | name: String 29 | } 30 | -------------------------------------------------------------------------------- /backend/src/utils.ts: -------------------------------------------------------------------------------- 1 | import { Prisma } from './generated/prisma-client' 2 | 3 | export interface Context { 4 | prisma: Prisma 5 | } 6 | -------------------------------------------------------------------------------- /backend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "moduleResolution": "node", 5 | "module": "commonjs", 6 | "sourceMap": true, 7 | "rootDir": "src", 8 | "outDir": "dist", 9 | "lib": [ 10 | "esnext", "dom" 11 | ] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@types/jest": "24.0.23", 7 | "@types/node": "12.12.14", 8 | "@types/react": "16.9.13", 9 | "@types/react-dom": "16.9.4", 10 | "apollo-boost": "^0.4.4", 11 | "graphql": "^14.5.8", 12 | "react": "^16.12.0", 13 | "react-apollo": "^3.1.3", 14 | "react-dom": "^16.12.0", 15 | "react-scripts": "3.2.0", 16 | "typescript": "3.7.2" 17 | }, 18 | "scripts": { 19 | "start": "react-scripts start", 20 | "build": "react-scripts build", 21 | "test": "react-scripts test", 22 | "eject": "react-scripts eject" 23 | }, 24 | "eslintConfig": { 25 | "extends": "react-app" 26 | }, 27 | "browserslist": { 28 | "production": [ 29 | ">0.2%", 30 | "not dead", 31 | "not op_mini all" 32 | ], 33 | "development": [ 34 | "last 1 chrome version", 35 | "last 1 firefox version", 36 | "last 1 safari version" 37 | ] 38 | }, 39 | "devDependencies": {} 40 | } 41 | -------------------------------------------------------------------------------- /frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adenh93/react-graphql-typescript-boilerplate/199f83240b0b95af8c384babc896f8f700f868c6/frontend/public/favicon.ico -------------------------------------------------------------------------------- /frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 13 | 14 | 23 | Hello React! 24 | 25 | 26 | 27 |
28 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /frontend/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Hello React!", 3 | "name": "Hello React!", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /frontend/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | -------------------------------------------------------------------------------- /frontend/src/assets/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adenh93/react-graphql-typescript-boilerplate/199f83240b0b95af8c384babc896f8f700f868c6/frontend/src/assets/.keep -------------------------------------------------------------------------------- /frontend/src/components/UserList/User/User.tsx: -------------------------------------------------------------------------------- 1 | import React, { SFC } from "react"; 2 | 3 | interface Props { 4 | name: string; 5 | username: string; 6 | email: string; 7 | } 8 | 9 | const User: SFC = ({ name, username, email }) => ( 10 |
11 |

{name}

12 |

{username}

13 |

{email}

14 |
15 | ); 16 | 17 | export default User; 18 | -------------------------------------------------------------------------------- /frontend/src/components/UserList/User/index.ts: -------------------------------------------------------------------------------- 1 | import User from "./User"; 2 | export default User; 3 | -------------------------------------------------------------------------------- /frontend/src/components/UserList/UserList.tsx: -------------------------------------------------------------------------------- 1 | import React, { SFC } from "react"; 2 | import UserComponent from "./User"; 3 | import { User } from "../../types"; 4 | 5 | interface Props { 6 | users: User[]; 7 | } 8 | 9 | const UserList: SFC = ({ users }) => ( 10 |
11 | {!users.length ? ( 12 |

No users to display.

13 | ) : ( 14 |
    15 | {users.map(({ id, ...user }) => ( 16 |
  • 17 | 18 |
  • 19 | ))} 20 |
21 | )} 22 |
23 | ); 24 | 25 | export default UserList; 26 | -------------------------------------------------------------------------------- /frontend/src/components/UserList/UserListContainer.tsx: -------------------------------------------------------------------------------- 1 | import React, { SFC } from "react"; 2 | import UserList from "./UserList"; 3 | import { useQuery } from "react-apollo"; 4 | import { getUsers } from "../../graphql/query/user"; 5 | import { Query } from "../../types"; 6 | 7 | const UserListContainer: SFC = () => { 8 | const { loading, error, data } = useQuery(getUsers); 9 | 10 | if (loading) return

Loading users...

; 11 | if (error) return

Error loading users...

; 12 | 13 | return ; 14 | }; 15 | 16 | export default UserListContainer; 17 | -------------------------------------------------------------------------------- /frontend/src/components/UserList/index.ts: -------------------------------------------------------------------------------- 1 | import UserListContainer from "./UserListContainer"; 2 | export default UserListContainer; 3 | -------------------------------------------------------------------------------- /frontend/src/graphql/fragments/user.ts: -------------------------------------------------------------------------------- 1 | import { gql } from "apollo-boost"; 2 | 3 | export const userFields = gql` 4 | fragment userFields on User { 5 | id 6 | username 7 | email 8 | name 9 | } 10 | `; 11 | -------------------------------------------------------------------------------- /frontend/src/graphql/mutation/user.ts: -------------------------------------------------------------------------------- 1 | import { gql } from "apollo-boost"; 2 | import { userFields } from "../fragments/user"; 3 | 4 | export const createUser = gql` 5 | mutation($data: CreateUserInput) { 6 | createUser(data: $data) { 7 | ...userFields 8 | } 9 | } 10 | ${userFields} 11 | `; 12 | 13 | export const updateUser = gql` 14 | mutation($id: ID!, $data: UpdateUserInput) { 15 | updateUser(id: $id, data: $data) { 16 | ...userFields 17 | } 18 | } 19 | ${userFields} 20 | `; 21 | 22 | export const deleteUser = gql` 23 | mutation($id: ID!) { 24 | deleteUser(id: $id) { 25 | ...userFields 26 | } 27 | } 28 | ${userFields} 29 | `; 30 | -------------------------------------------------------------------------------- /frontend/src/graphql/query/user.ts: -------------------------------------------------------------------------------- 1 | import { gql } from "apollo-boost"; 2 | import { userFields } from "../fragments/user"; 3 | 4 | export const getUsers = gql` 5 | query { 6 | users { 7 | ...userFields 8 | } 9 | } 10 | ${userFields} 11 | `; 12 | 13 | export const getUser = gql` 14 | query($id: ID!) { 15 | user(id: $id) { 16 | ...userFields 17 | } 18 | } 19 | ${userFields} 20 | `; 21 | -------------------------------------------------------------------------------- /frontend/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /frontend/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import "./index.css"; 4 | import App from "./pages/App"; 5 | import * as serviceWorker from "./serviceWorker"; 6 | 7 | import { ApolloProvider } from "react-apollo"; 8 | import ApolloBoost from "apollo-boost"; 9 | 10 | const client = new ApolloBoost({ 11 | uri: "http://localhost:4000" 12 | }); 13 | 14 | ReactDOM.render( 15 | 16 | 17 | , 18 | document.getElementById("root") 19 | ); 20 | 21 | serviceWorker.unregister(); 22 | -------------------------------------------------------------------------------- /frontend/src/pages/App/App.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adenh93/react-graphql-typescript-boilerplate/199f83240b0b95af8c384babc896f8f700f868c6/frontend/src/pages/App/App.css -------------------------------------------------------------------------------- /frontend/src/pages/App/App.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | it('renders without crashing', () => { 6 | const div = document.createElement('div'); 7 | ReactDOM.render(, div); 8 | ReactDOM.unmountComponentAtNode(div); 9 | }); 10 | -------------------------------------------------------------------------------- /frontend/src/pages/App/App.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "./App.css"; 3 | import UserList from "../../components/UserList"; 4 | 5 | const App: React.FC = () => { 6 | return ( 7 |
8 |

Hello React!

9 | 10 |
11 | ); 12 | }; 13 | 14 | export default App; 15 | -------------------------------------------------------------------------------- /frontend/src/pages/App/index.ts: -------------------------------------------------------------------------------- 1 | import App from "./App" 2 | export default App; -------------------------------------------------------------------------------- /frontend/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /frontend/src/serviceWorker.ts: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.1/8 is considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | type Config = { 24 | onSuccess?: (registration: ServiceWorkerRegistration) => void; 25 | onUpdate?: (registration: ServiceWorkerRegistration) => void; 26 | }; 27 | 28 | export function register(config?: Config) { 29 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 30 | // The URL constructor is available in all browsers that support SW. 31 | const publicUrl = new URL( 32 | (process as { env: { [key: string]: string } }).env.PUBLIC_URL, 33 | window.location.href 34 | ); 35 | if (publicUrl.origin !== window.location.origin) { 36 | // Our service worker won't work if PUBLIC_URL is on a different origin 37 | // from what our page is served on. This might happen if a CDN is used to 38 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 39 | return; 40 | } 41 | 42 | window.addEventListener('load', () => { 43 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 44 | 45 | if (isLocalhost) { 46 | // This is running on localhost. Let's check if a service worker still exists or not. 47 | checkValidServiceWorker(swUrl, config); 48 | 49 | // Add some additional logging to localhost, pointing developers to the 50 | // service worker/PWA documentation. 51 | navigator.serviceWorker.ready.then(() => { 52 | console.log( 53 | 'This web app is being served cache-first by a service ' + 54 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 55 | ); 56 | }); 57 | } else { 58 | // Is not localhost. Just register service worker 59 | registerValidSW(swUrl, config); 60 | } 61 | }); 62 | } 63 | } 64 | 65 | function registerValidSW(swUrl: string, config?: Config) { 66 | navigator.serviceWorker 67 | .register(swUrl) 68 | .then(registration => { 69 | registration.onupdatefound = () => { 70 | const installingWorker = registration.installing; 71 | if (installingWorker == null) { 72 | return; 73 | } 74 | installingWorker.onstatechange = () => { 75 | if (installingWorker.state === 'installed') { 76 | if (navigator.serviceWorker.controller) { 77 | // At this point, the updated precached content has been fetched, 78 | // but the previous service worker will still serve the older 79 | // content until all client tabs are closed. 80 | console.log( 81 | 'New content is available and will be used when all ' + 82 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 83 | ); 84 | 85 | // Execute callback 86 | if (config && config.onUpdate) { 87 | config.onUpdate(registration); 88 | } 89 | } else { 90 | // At this point, everything has been precached. 91 | // It's the perfect time to display a 92 | // "Content is cached for offline use." message. 93 | console.log('Content is cached for offline use.'); 94 | 95 | // Execute callback 96 | if (config && config.onSuccess) { 97 | config.onSuccess(registration); 98 | } 99 | } 100 | } 101 | }; 102 | }; 103 | }) 104 | .catch(error => { 105 | console.error('Error during service worker registration:', error); 106 | }); 107 | } 108 | 109 | function checkValidServiceWorker(swUrl: string, config?: Config) { 110 | // Check if the service worker can be found. If it can't reload the page. 111 | fetch(swUrl) 112 | .then(response => { 113 | // Ensure service worker exists, and that we really are getting a JS file. 114 | const contentType = response.headers.get('content-type'); 115 | if ( 116 | response.status === 404 || 117 | (contentType != null && contentType.indexOf('javascript') === -1) 118 | ) { 119 | // No service worker found. Probably a different app. Reload the page. 120 | navigator.serviceWorker.ready.then(registration => { 121 | registration.unregister().then(() => { 122 | window.location.reload(); 123 | }); 124 | }); 125 | } else { 126 | // Service worker found. Proceed as normal. 127 | registerValidSW(swUrl, config); 128 | } 129 | }) 130 | .catch(() => { 131 | console.log( 132 | 'No internet connection found. App is running in offline mode.' 133 | ); 134 | }); 135 | } 136 | 137 | export function unregister() { 138 | if ('serviceWorker' in navigator) { 139 | navigator.serviceWorker.ready.then(registration => { 140 | registration.unregister(); 141 | }); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /frontend/src/types.ts: -------------------------------------------------------------------------------- 1 | export type Maybe = T | null; 2 | /** All built-in and custom scalars, mapped to their actual values */ 3 | export type Scalars = { 4 | ID: string, 5 | String: string, 6 | Boolean: boolean, 7 | Int: number, 8 | Float: number, 9 | }; 10 | 11 | export type CreateUserInput = { 12 | username: Scalars['String'], 13 | email: Scalars['String'], 14 | name: Scalars['String'], 15 | }; 16 | 17 | export type Mutation = { 18 | __typename?: 'Mutation', 19 | createUser?: Maybe, 20 | updateUser?: Maybe, 21 | deleteUser?: Maybe, 22 | }; 23 | 24 | 25 | export type MutationCreateUserArgs = { 26 | data?: Maybe 27 | }; 28 | 29 | 30 | export type MutationUpdateUserArgs = { 31 | id?: Maybe, 32 | data?: Maybe 33 | }; 34 | 35 | 36 | export type MutationDeleteUserArgs = { 37 | id?: Maybe 38 | }; 39 | 40 | export type Query = { 41 | __typename?: 'Query', 42 | user?: Maybe, 43 | users: Array, 44 | }; 45 | 46 | 47 | export type QueryUserArgs = { 48 | id: Scalars['ID'] 49 | }; 50 | 51 | export type UpdateUserInput = { 52 | username?: Maybe, 53 | email?: Maybe, 54 | name?: Maybe, 55 | }; 56 | 57 | export type User = { 58 | __typename?: 'User', 59 | id: Scalars['ID'], 60 | username: Scalars['String'], 61 | email: Scalars['String'], 62 | name: Scalars['String'], 63 | }; 64 | -------------------------------------------------------------------------------- /frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "noEmit": true, 20 | "jsx": "react" 21 | }, 22 | "include": [ 23 | "src" 24 | ] 25 | } 26 | --------------------------------------------------------------------------------