From 20f341ec782bdd313e27b97fc8882c40c75efde4 Mon Sep 17 00:00:00 2001 From: Robin Wieruch Date: Wed, 15 Apr 2020 17:27:48 +0200 Subject: [PATCH 01/13] add dependencies for typegraphql --- package-lock.json | 121 +++++++++++++++++++++++++++++++++++++++------- package.json | 6 ++- 2 files changed, 108 insertions(+), 19 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1a482717..e9476e65 100644 --- a/package-lock.json +++ b/package-lock.json @@ -530,12 +530,13 @@ } }, "@babel/plugin-proposal-class-properties": { - "version": "7.7.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.7.0.tgz", - "integrity": "sha512-tufDcFA1Vj+eWvwHN+jvMN6QsV5o+vUlytNKrbMiCeDL0F2j92RURzUsUMWE5EJkLyWxjdUslCsMQa9FWth16A==", + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.8.3.tgz", + "integrity": "sha512-EqFhbo7IosdgPgZggHaNObkmO1kNUe3slaKu54d5OWvy+p9QIKOzK1GAEpAIsZtWVtPXUHSMcT4smvDrCfY4AA==", + "dev": true, "requires": { - "@babel/helper-create-class-features-plugin": "^7.7.0", - "@babel/helper-plugin-utils": "^7.0.0" + "@babel/helper-create-class-features-plugin": "^7.8.3", + "@babel/helper-plugin-utils": "^7.8.3" } }, "@babel/plugin-proposal-decorators": { @@ -3539,6 +3540,11 @@ "integrity": "sha512-bWG5wapaWgbss9E238T0R6bfo5Fh3OkeoSt245CM7JJwVwpw6MEBCbIxLq5z8KzsE3uJhzcIuQkyiZmzV3M/Dw==", "dev": true }, + "@types/events": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.0.tgz", + "integrity": "sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==" + }, "@types/express": { "version": "4.17.3", "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.3.tgz", @@ -3580,12 +3586,14 @@ "@types/node": "*" } }, - "@types/graphql-iso-date": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/@types/graphql-iso-date/-/graphql-iso-date-3.3.3.tgz", - "integrity": "sha512-lchvlAox/yqk2Rcrgqh+uvwc1UC9i1hap+0tqQqyYYcAica6Uw2D4mUkCNcw+WeZ8dvSS5QdtIlJuDYUf4nLXQ==", + "@types/glob": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.1.tgz", + "integrity": "sha512-1Bh06cbWJUHMC97acuD6UMG29nMt0Aqz1vF3guLfG+kHHJhy3AyohZFFxYk2f7Q1SQIrNwvncxAE0N/9s70F2w==", "requires": { - "graphql": "^14.5.3" + "@types/events": "*", + "@types/minimatch": "*", + "@types/node": "*" } }, "@types/graphql-upload": { @@ -3781,6 +3789,11 @@ "resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.1.tgz", "integrity": "sha512-FwI9gX75FgVBJ7ywgnq/P7tw+/o1GUbtP0KzbtusLigAOgIgNISRK0ZPl4qertvXSIE8YbsVJueQ90cDt9YYyw==" }, + "@types/minimatch": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz", + "integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==" + }, "@types/mkdirp": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@types/mkdirp/-/mkdirp-1.0.0.tgz", @@ -3886,6 +3899,14 @@ "@types/react": "*" } }, + "@types/semver": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.1.0.tgz", + "integrity": "sha512-pOKLaubrAEMUItGNpgwl0HMFPrSAFic8oSVIvfu1UwcgGNmNyK9gyhBHKmBnUTwwVvpZfkzUC0GaMgnL6P86uA==", + "requires": { + "@types/node": "*" + } + }, "@types/serve-static": { "version": "1.13.3", "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.3.tgz", @@ -3973,6 +3994,11 @@ "integrity": "sha1-YPpDXOJL/VuhB7jSqAeWrq86j0U=", "dev": true }, + "@types/validator": { + "version": "10.11.3", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-10.11.3.tgz", + "integrity": "sha512-GKF2VnEkMmEeEGvoo03ocrP9ySMuX1ypKazIYMlsjfslfBMhOAtC5dmEWKdJioW4lJN7MZRS88kalTsVClyQ9w==" + }, "@types/ws": { "version": "6.0.4", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-6.0.4.tgz", @@ -5825,6 +5851,14 @@ "resolved": "https://registry.npmjs.org/babel-plugin-transform-react-remove-prop-types/-/babel-plugin-transform-react-remove-prop-types-0.4.24.tgz", "integrity": "sha512-eqj0hVcJUR57/Ug2zE1Yswsw4LhuqqHhD+8v120T1cl3kjg76QwtyBrdIk4WVwK+lAhBJVYCd/v+4nc4y+8JsA==" }, + "babel-plugin-transform-typescript-metadata": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-typescript-metadata/-/babel-plugin-transform-typescript-metadata-0.3.0.tgz", + "integrity": "sha512-ASYrM+bxtpfgZKsAOqQfjmLlekIDigRnNCfQBDOOdaqL18hLhZIsbdiHsuaNDTkljlqnbV/DlufaWY55jC2PBg==", + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, "babel-polyfill": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-polyfill/-/babel-polyfill-6.26.0.tgz", @@ -6831,6 +6865,16 @@ } } }, + "class-validator": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.11.1.tgz", + "integrity": "sha512-6CGdjwJLmKw+sQbK5ZDo1v1yTajkqfPOUDWSYVIlhUiCh6Phy8sAnMFE2XKHAcKAdoOz4jJUQhjPQWPYUuHxrA==", + "requires": { + "@types/validator": "10.11.3", + "google-libphonenumber": "^3.1.6", + "validator": "12.0.0" + } + }, "classnames": { "version": "2.2.6", "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.2.6.tgz", @@ -10065,6 +10109,11 @@ } } }, + "google-libphonenumber": { + "version": "3.2.8", + "resolved": "https://registry.npmjs.org/google-libphonenumber/-/google-libphonenumber-3.2.8.tgz", + "integrity": "sha512-iWs1KcxOozmKQbCeGjvU0M7urrkNjBYOSBtb819RjkUNJHJLfn7DADKkKwdJTOMPLcLOE11/4h/FyFwJsTiwLg==" + }, "google-p12-pem": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-2.0.4.tgz", @@ -10307,11 +10356,6 @@ } } }, - "graphql-iso-date": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/graphql-iso-date/-/graphql-iso-date-3.6.1.tgz", - "integrity": "sha512-AwFGIuYMJQXOEAgRlJlFL4H1ncFM8n8XmoVDTNypNOZyQ8LFDG2ppMFlsS862BSTCDcSUfHp8PD3/uJhv7t59Q==" - }, "graphql-middleware": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/graphql-middleware/-/graphql-middleware-4.0.2.tgz", @@ -10325,6 +10369,14 @@ "resolved": "https://registry.npmjs.org/graphql-middleware-sentry/-/graphql-middleware-sentry-3.2.1.tgz", "integrity": "sha512-lAwmHwsyey1db6scQg32javmqAFifabhqPIr0SUzx46O4kvjQlLZZn7KrRT12XDwgW7i6goAotdSPl9Fq+TBrQ==" }, + "graphql-query-complexity": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/graphql-query-complexity/-/graphql-query-complexity-0.4.1.tgz", + "integrity": "sha512-Uo87hNlnJ5jwoWBkVYITbJpTrlCVwgfG5Wrfel0K1/42G+3xvud31CpsprAwiSpFIP+gCqttAx7OVmw4eTqLQQ==", + "requires": { + "lodash.get": "^4.4.2" + } + }, "graphql-request": { "version": "1.8.2", "resolved": "https://registry.npmjs.org/graphql-request/-/graphql-request-1.8.2.tgz", @@ -14590,8 +14642,7 @@ "lodash.get": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", - "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=", - "dev": true + "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=" }, "lodash.has": { "version": "4.5.2", @@ -15648,6 +15699,15 @@ } } }, + "@babel/plugin-proposal-class-properties": { + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.7.0.tgz", + "integrity": "sha512-tufDcFA1Vj+eWvwHN+jvMN6QsV5o+vUlytNKrbMiCeDL0F2j92RURzUsUMWE5EJkLyWxjdUslCsMQa9FWth16A==", + "requires": { + "@babel/helper-create-class-features-plugin": "^7.7.0", + "@babel/helper-plugin-utils": "^7.0.0" + } + }, "@babel/plugin-proposal-object-rest-spread": { "version": "7.6.2", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.6.2.tgz", @@ -21917,6 +21977,28 @@ "integrity": "sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ==", "dev": true }, + "type-graphql": { + "version": "0.18.0-beta.16", + "resolved": "https://registry.npmjs.org/type-graphql/-/type-graphql-0.18.0-beta.16.tgz", + "integrity": "sha512-tieNkmY2r74cKpFjzwgsx5FpjklxhCq37BqCB5qc2ky75od4I/JkRpdElyWxfcSerBfNjcwcovTFBrRoYrP65Q==", + "requires": { + "@types/glob": "^7.1.1", + "@types/node": "*", + "@types/semver": "^7.1.0", + "glob": "^7.1.6", + "graphql-query-complexity": "^0.4.1", + "graphql-subscriptions": "^1.1.0", + "semver": "^7.1.3", + "tslib": "^1.11.1" + }, + "dependencies": { + "semver": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz", + "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==" + } + } + }, "type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", @@ -22510,6 +22592,11 @@ "spdx-expression-parse": "^3.0.0" } }, + "validator": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-12.0.0.tgz", + "integrity": "sha512-r5zA1cQBEOgYlesRmSEwc9LkbfNLTtji+vWyaHzRZUxCTHdsX3bd+sdHfs5tGZ2W6ILGGsxWxCNwT/h3IY/3ng==" + }, "vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", diff --git a/package.json b/package.json index 9407690e..4c948763 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,6 @@ "@sentry/browser": "^5.12.4", "@sentry/node": "^5.12.4", "@types/file-saver": "^2.0.1", - "@types/graphql-iso-date": "^3.3.3", "@types/js-cookie": "^2.2.4", "@types/lodash.invert": "^4.3.6", "@types/lodash.omit": "^4.5.6", @@ -47,12 +46,13 @@ "b64-to-blob": "^1.2.19", "babel-plugin-import": "^1.13.0", "babel-plugin-inline-import": "^3.0.0", + "babel-plugin-transform-typescript-metadata": "^0.3.0", + "class-validator": "^0.11.1", "dotenv": "^8.2.0", "file-saver": "^2.0.2", "firebase": "^7.7.0", "firebase-admin": "^8.9.1", "graphql": "^14.5.8", - "graphql-iso-date": "^3.6.1", "graphql-middleware": "^4.0.2", "graphql-middleware-sentry": "^3.2.1", "graphql-shield": "^7.0.9", @@ -84,10 +84,12 @@ "reflect-metadata": "^0.1.13", "stripe": "^8.7.0", "styled-components": "^5.0.0", + "type-graphql": "^0.18.0-beta.16", "typeorm": "^0.2.24" }, "devDependencies": { "@apollo/react-testing": "^3.1.3", + "@babel/plugin-proposal-class-properties": "^7.8.3", "@babel/plugin-proposal-decorators": "^7.8.3", "@graphql-codegen/add": "^1.12.1", "@graphql-codegen/cli": "^1.12.1", From 19d5e3b21b57099dd7510e783d51fdf78d50ce29 Mon Sep 17 00:00:00 2001 From: Robin Wieruch Date: Wed, 15 Apr 2020 17:28:21 +0200 Subject: [PATCH 02/13] add decorator capability --- .babelrc | 7 +++++++ tsconfig.json | 10 ++++++++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/.babelrc b/.babelrc index 560ca1e7..3ef5ee7e 100644 --- a/.babelrc +++ b/.babelrc @@ -45,11 +45,18 @@ "style": true } ], + "babel-plugin-transform-typescript-metadata", [ "@babel/plugin-proposal-decorators", { "legacy": true } + ], + [ + "@babel/plugin-proposal-class-properties", + { + "loose": true + } ] ] } diff --git a/tsconfig.json b/tsconfig.json index 90ac2f72..f1d18e70 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,7 +1,13 @@ { "compilerOptions": { - "target": "es5", - "lib": ["es6", "dom", "dom.iterable", "esnext"], + "target": "es6", + "lib": [ + "dom", + "dom.iterable", + "es6", + "es2017", + "esnext.asynciterable" + ], "allowJs": true, "skipLibCheck": true, "strict": true, From 5f81f0c279e2243168b17ff09ebcfe6ea33bf9ca Mon Sep 17 00:00:00 2001 From: Robin Wieruch Date: Wed, 15 Apr 2020 17:28:46 +0200 Subject: [PATCH 03/13] separate types from data --- src/data/bundle-keys-types.ts | 1 + src/data/bundle-keys.ts | 2 -- src/data/course-keys-types.ts | 12 ++++++++++++ src/data/course-keys.ts | 6 ------ 4 files changed, 13 insertions(+), 8 deletions(-) create mode 100644 src/data/bundle-keys-types.ts create mode 100644 src/data/course-keys-types.ts diff --git a/src/data/bundle-keys-types.ts b/src/data/bundle-keys-types.ts new file mode 100644 index 00000000..d4733fcb --- /dev/null +++ b/src/data/bundle-keys-types.ts @@ -0,0 +1 @@ +export type BUNDLE = 'STUDENT' | 'INTERMEDIATE' | 'PROFESSIONAL'; diff --git a/src/data/bundle-keys.ts b/src/data/bundle-keys.ts index 5a924fb0..ccc33827 100644 --- a/src/data/bundle-keys.ts +++ b/src/data/bundle-keys.ts @@ -31,5 +31,3 @@ export const THE_ROAD_TO_REACT_WITH_FIREBASE_BUNDLE_KEYS = { // RETIRED // BOOK: 'BOOK', }; - -export type BUNDLE = 'STUDENT' | 'INTERMEDIATE' | 'PROFESSIONAL'; diff --git a/src/data/course-keys-types.ts b/src/data/course-keys-types.ts new file mode 100644 index 00000000..81298961 --- /dev/null +++ b/src/data/course-keys-types.ts @@ -0,0 +1,12 @@ +import { + THE_ROAD_TO_LEARN_REACT, + TAMING_THE_STATE, + THE_ROAD_TO_GRAPHQL, + THE_ROAD_TO_REACT_WITH_FIREBASE, +} from './course-keys'; + +export type COURSE = + | typeof THE_ROAD_TO_LEARN_REACT + | typeof TAMING_THE_STATE + | typeof THE_ROAD_TO_GRAPHQL + | typeof THE_ROAD_TO_REACT_WITH_FIREBASE; diff --git a/src/data/course-keys.ts b/src/data/course-keys.ts index fa4c48ff..3a86d963 100644 --- a/src/data/course-keys.ts +++ b/src/data/course-keys.ts @@ -3,9 +3,3 @@ export const TAMING_THE_STATE = 'TAMING_THE_STATE'; export const THE_ROAD_TO_GRAPHQL = 'THE_ROAD_TO_GRAPHQL'; export const THE_ROAD_TO_REACT_WITH_FIREBASE = 'THE_ROAD_TO_REACT_WITH_FIREBASE'; - -export type COURSE = - | typeof THE_ROAD_TO_LEARN_REACT - | typeof TAMING_THE_STATE - | typeof THE_ROAD_TO_GRAPHQL - | typeof THE_ROAD_TO_REACT_WITH_FIREBASE; From 6be22aca6202cc2eefedd997e590ff61ff34e44d Mon Sep 17 00:00:00 2001 From: Robin Wieruch Date: Wed, 15 Apr 2020 17:29:00 +0200 Subject: [PATCH 04/13] remove graphql-code-gen for server --- codegen.yml | 8 -------- 1 file changed, 8 deletions(-) diff --git a/codegen.yml b/codegen.yml index be1b7a11..87f15bc0 100644 --- a/codegen.yml +++ b/codegen.yml @@ -1,14 +1,6 @@ schema: http://localhost:3000/api/graphql generates: - src/generated/server.ts: - config: - defaultMapper: any - contextType: @typeDefs/resolver#ResolverContext - useIndexSignature: true - plugins: - - typescript - - typescript-resolvers src/generated/client.tsx: documents: ./src/queries/*.ts config: From fc96a5bb8cf961cfaea05422cc13ea72dd9b659f Mon Sep 17 00:00:00 2001 From: Robin Wieruch Date: Wed, 15 Apr 2020 17:30:17 +0200 Subject: [PATCH 05/13] wip --- pages/api/graphql.ts | 10 +- src/api/authorization/isFreeCourse.ts | 4 +- src/api/resolvers/book/index.ts | 92 +- src/api/resolvers/community/index.ts | 41 +- src/api/resolvers/coupon/index.ts | 117 ++- .../course/__snapshots__/spec.ts.snap | 37 - src/api/resolvers/course/index.ts | 429 ++++++--- src/api/resolvers/course/spec.ts | 254 ----- src/api/resolvers/index.ts | 54 +- src/api/resolvers/migration/index.ts | 32 +- src/api/resolvers/partner/index.ts | 267 ++++-- src/api/resolvers/paypal/index.ts | 240 ++--- src/api/resolvers/session/index.ts | 222 +++-- .../storefront/__snapshots__/spec.ts.snap | 22 - src/api/resolvers/storefront/index.ts | 128 ++- src/api/resolvers/storefront/spec.ts | 44 - src/api/resolvers/stripe/index.ts | 138 +-- src/api/resolvers/upgrade/index.ts | 45 +- .../resolvers/user/__snapshots__/spec.ts.snap | 22 - src/api/resolvers/user/index.ts | 57 +- src/api/resolvers/user/spec.ts | 44 - src/api/schema/index.ts | 92 +- src/api/typeDefs/book/index.ts | 19 - src/api/typeDefs/community/index.ts | 7 - src/api/typeDefs/coupon/index.ts | 24 - src/api/typeDefs/course/index.ts | 127 --- src/api/typeDefs/migration/index.ts | 7 - src/api/typeDefs/partner/index.ts | 43 - src/api/typeDefs/paypal/index.ts | 18 - src/api/typeDefs/session/index.ts | 23 - src/api/typeDefs/storefront/index.ts | 31 - src/api/typeDefs/stripe/index.ts | 17 - src/api/typeDefs/upgrade/index.ts | 7 - src/api/typeDefs/user/index.ts | 14 - src/connectors/course.ts | 4 +- src/connectors/partner.ts | 2 +- src/generated/client.tsx | 203 ++-- src/generated/server.ts | 889 ------------------ src/models/course.ts | 4 +- src/models/index.ts | 1 - src/queries/coupon.ts | 8 +- src/queries/course.ts | 11 +- src/queries/partner.ts | 2 +- src/queries/paypal.ts | 4 +- src/queries/storefront.ts | 5 +- src/queries/stripe.ts | 4 +- src/queries/upgrade.ts | 2 +- src/services/course/index.ts | 2 +- src/services/discount/index.ts | 4 +- src/services/firebase/course.ts | 4 +- src/types/storefront.ts | 15 - 51 files changed, 1336 insertions(+), 2556 deletions(-) delete mode 100644 src/api/resolvers/course/__snapshots__/spec.ts.snap delete mode 100644 src/api/resolvers/course/spec.ts delete mode 100644 src/api/resolvers/storefront/__snapshots__/spec.ts.snap delete mode 100644 src/api/resolvers/storefront/spec.ts delete mode 100644 src/api/resolvers/user/__snapshots__/spec.ts.snap delete mode 100644 src/api/resolvers/user/spec.ts delete mode 100644 src/api/typeDefs/book/index.ts delete mode 100644 src/api/typeDefs/community/index.ts delete mode 100644 src/api/typeDefs/coupon/index.ts delete mode 100644 src/api/typeDefs/course/index.ts delete mode 100644 src/api/typeDefs/migration/index.ts delete mode 100644 src/api/typeDefs/partner/index.ts delete mode 100644 src/api/typeDefs/paypal/index.ts delete mode 100644 src/api/typeDefs/session/index.ts delete mode 100644 src/api/typeDefs/storefront/index.ts delete mode 100644 src/api/typeDefs/stripe/index.ts delete mode 100644 src/api/typeDefs/upgrade/index.ts delete mode 100644 src/api/typeDefs/user/index.ts delete mode 100644 src/generated/server.ts delete mode 100644 src/types/storefront.ts diff --git a/pages/api/graphql.ts b/pages/api/graphql.ts index eed1882e..fab9828b 100644 --- a/pages/api/graphql.ts +++ b/pages/api/graphql.ts @@ -1,5 +1,7 @@ import { ApolloServer } from 'apollo-server-micro'; import cors from 'micro-cors'; +import { buildSchema } from 'type-graphql'; +import 'reflect-metadata'; import getConnection from '@models/index'; import { AdminConnector } from '@connectors/admin'; @@ -8,7 +10,8 @@ import { CourseConnector } from '@connectors/course'; import { CouponConnector } from '@connectors/coupon'; import { ServerRequest, ServerResponse } from '@typeDefs/server'; import { ResolverContext } from '@typeDefs/resolver'; -import schema from '@api/schema'; + +import resolvers from '@api/resolvers'; import getMe from '@api/middleware/getMe'; import firebaseAdmin from '@services/firebase/admin'; @@ -41,6 +44,11 @@ export const config = { export default async (req: ServerRequest, res: ServerResponse) => { const connection = await getConnection(); + const schema = await buildSchema({ + resolvers, + dateScalarMode: 'isoDate', + }); + const server = new ApolloServer({ schema, context: async ({ req, res }): Promise => { diff --git a/src/api/authorization/isFreeCourse.ts b/src/api/authorization/isFreeCourse.ts index 7b57e010..6944e98a 100644 --- a/src/api/authorization/isFreeCourse.ts +++ b/src/api/authorization/isFreeCourse.ts @@ -1,8 +1,8 @@ import { rule } from 'graphql-shield'; import storefront from '@data/course-storefront'; -import { COURSE } from '@data/course-keys'; -import { BUNDLE } from '@data/bundle-keys'; +import { COURSE } from '@data/course-keys-types'; +import { BUNDLE } from '@data/bundle-keys-types'; export const isFreeCourse = rule()( async ( diff --git a/src/api/resolvers/book/index.ts b/src/api/resolvers/book/index.ts index 57800594..5b4d625b 100644 --- a/src/api/resolvers/book/index.ts +++ b/src/api/resolvers/book/index.ts @@ -1,37 +1,63 @@ -import { QueryResolvers } from '@generated/server'; import s3, { bucket } from '@services/aws/s3'; -interface Resolvers { - Query: QueryResolvers; +import { + ObjectType, + Field, + Arg, + Resolver, + Query, +} from 'type-graphql'; + +@ObjectType() +class File { + @Field() + fileName: string; + + @Field() + contentType: string; + + @Field() + body: string; +} + +@ObjectType() +class Markdown { + @Field() + body: string; } -export const resolvers: Resolvers = { - Query: { - book: async (_, { path, fileName }) => { - const data = await s3 - .getObject({ - Bucket: bucket, - Key: path, - }) - .promise(); - - return { - fileName, - contentType: data.ContentType, - body: data?.Body?.toString('base64'), - }; - }, - onlineChapter: async (_, { path }) => { - const data = await s3 - .getObject({ - Bucket: bucket, - Key: path, - }) - .promise(); - - return { - body: data?.Body?.toString('base64'), - }; - }, - }, -}; +@Resolver() +export default class BookResolver { + @Query(() => File) + async book( + @Arg('path') path: string, + @Arg('fileName') fileName: string + ) { + const data = await s3 + .getObject({ + Bucket: bucket, + Key: path, + }) + .promise(); + + return { + fileName, + contentType: data.ContentType, + body: data?.Body?.toString('base64'), + }; + } + + @Query(() => Markdown) + async onlineChapter(@Arg('path') path: string) { + const data = await s3 + .getObject({ + Bucket: bucket, + Key: path, + }) + .promise(); + + return { + body: data?.Body?.toString('base64'), + }; + } +} diff --git a/src/api/resolvers/community/index.ts b/src/api/resolvers/community/index.ts index 54454906..80925efc 100644 --- a/src/api/resolvers/community/index.ts +++ b/src/api/resolvers/community/index.ts @@ -1,9 +1,6 @@ -import { MutationResolvers } from '@generated/server'; -import { inviteToSlack } from '@services/slack'; +import { Arg, Resolver, Mutation } from 'type-graphql'; -interface Resolvers { - Mutation: MutationResolvers; -} +import { inviteToSlack } from '@services/slack'; // https://api.slack.com/methods/admin.users.invite const SLACK_ERRORS: { [key: string]: string } = { @@ -61,24 +58,24 @@ const SLACK_ERRORS: { [key: string]: string } = { "The server could not complete your operation(s) without encountering an error, likely due to a transient issue on our end. It's possible some aspect of the operation succeeded before the error was raised.", }; -export const resolvers: Resolvers = { - Mutation: { - communityJoin: async (_, { email }) => { - try { - const result = await inviteToSlack(email); +@Resolver() +export default class CommunityResolver { + @Mutation(() => Boolean) + async communityJoin(@Arg('email') email: string) { + try { + const result = await inviteToSlack(email); - if (!result) { - return new Error('Something went wrong.'); - } + if (!result) { + return new Error('Something went wrong.'); + } - if (!result.data.ok) { - return new Error(SLACK_ERRORS[result.data.error]); - } - } catch (error) { - return new Error(error); + if (!result.data.ok) { + return new Error(SLACK_ERRORS[result.data.error]); } + } catch (error) { + return new Error(error); + } - return true; - }, - }, -}; + return true; + } +} diff --git a/src/api/resolvers/coupon/index.ts b/src/api/resolvers/coupon/index.ts index 4ce5e56f..b8f38bd1 100644 --- a/src/api/resolvers/coupon/index.ts +++ b/src/api/resolvers/coupon/index.ts @@ -1,50 +1,77 @@ -import { QueryResolvers, MutationResolvers } from '@generated/server'; +import { + ObjectType, + Field, + Arg, + Ctx, + Resolver, + Query, + Mutation, +} from 'type-graphql'; + +import { ResolverContext } from '@typeDefs/resolver'; import { priceWithDiscount } from '@services/discount'; import storefront from '@data/course-storefront'; +import { COURSE } from '@data/course-keys-types'; +import { BUNDLE } from '@data/bundle-keys-types'; +@ObjectType() +class Discount { + @Field() + price: number; -interface Resolvers { - Query: QueryResolvers; - Mutation: MutationResolvers; + @Field() + isDiscount: boolean; } -export const resolvers: Resolvers = { - Query: { - discountedPrice: async ( - _, - { courseId, bundleId, coupon }, - { me, courseConnector, couponConnector } - ) => { - const course = storefront[courseId]; - const bundle = course.bundles[bundleId]; - - if (!me) { - return bundle.price; - } - - const price = await priceWithDiscount( - couponConnector, - courseConnector - )(courseId, bundleId, bundle.price, coupon, me.uid); - - return { - price, - isDiscount: price !== bundle.price, - }; - }, - }, - Mutation: { - couponCreate: async ( - _, - { coupon, discount, count }, - { couponConnector } - ) => { - try { - await couponConnector.createCoupons(coupon, discount, count); - } catch (error) { - throw new Error(error); - } - - return true; - }, - }, -}; +@Resolver() +export default class CouponResolver { + @Query(() => Discount) + async discountedPrice( + @Arg('courseId') courseId: string, + @Arg('bundleId') bundleId: string, + @Arg('coupon') coupon: string, + @Ctx() ctx: ResolverContext + ) { + const course = storefront[courseId as COURSE]; + const bundle = course.bundles[bundleId as BUNDLE]; + + if (!ctx.me) { + return bundle.price; + } + + const price = await priceWithDiscount( + ctx.couponConnector, + ctx.courseConnector + )( + courseId as COURSE, + bundleId as BUNDLE, + bundle.price, + coupon, + ctx.me.uid + ); + + return { + price, + isDiscount: price !== bundle.price, + }; + } + + @Mutation(() => Boolean, { nullable: true }) + async couponCreate( + @Arg('coupon') coupon: string, + @Arg('discount') discount: number, + @Arg('count') count: number, + @Ctx() ctx: ResolverContext + ) { + try { + await ctx.couponConnector.createCoupons( + coupon, + discount, + count + ); + } catch (error) { + throw new Error(error); + } + + return true; + } +} diff --git a/src/api/resolvers/course/__snapshots__/spec.ts.snap b/src/api/resolvers/course/__snapshots__/spec.ts.snap deleted file mode 100644 index 363bec00..00000000 --- a/src/api/resolvers/course/__snapshots__/spec.ts.snap +++ /dev/null @@ -1,37 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`createAdminCourse creates a course 1`] = ` -Object { - "createAdminCourse": true, -} -`; - -exports[`createAdminCourse creates a course that is not for free 1`] = ` -Object { - "createAdminCourse": true, -} -`; - -exports[`createAdminCourse does not create a course if not admin 1`] = ` -Array [ - [GraphQLError: No admin user.], -] -`; - -exports[`createAdminCourse does not create a course if not authenticated 1`] = ` -Array [ - [GraphQLError: Not authenticated as user.], -] -`; - -exports[`createFreeCourse does not create a course if not authenticated 1`] = ` -Array [ - [GraphQLError: Not authenticated as user.], -] -`; - -exports[`createFreeCourse does not create a course if not free 1`] = ` -Array [ - [GraphQLError: Not authenticated as user.], -] -`; diff --git a/src/api/resolvers/course/index.ts b/src/api/resolvers/course/index.ts index e426e3b5..16746d2b 100644 --- a/src/api/resolvers/course/index.ts +++ b/src/api/resolvers/course/index.ts @@ -1,116 +1,319 @@ -import { QueryResolvers, MutationResolvers } from '@generated/server'; +import { + ObjectType, + Field, + Arg, + Ctx, + Resolver, + Query, + Mutation, +} from 'type-graphql'; + +import { StorefrontCourse } from '@api/resolvers/storefront'; +import { ResolverContext } from '@typeDefs/resolver'; import { createCourse } from '@services/firebase/course'; import { mergeCourses } from '@services/course'; +import { COURSE } from '@data/course-keys-types'; +import { BUNDLE } from '@data/bundle-keys-types'; + +@ObjectType() +class CurriculumItem { + @Field() + label: string; + + @Field() + url: string; + + @Field() + description: string; + + @Field() + kind: 'Article' | 'Video'; + + @Field({ nullable: true }) + secondaryUrl: string; +} + +@ObjectType() +class CurriculumSection { + @Field() + label: string; + + @Field(type => [CurriculumItem]) + items: CurriculumItem[]; +} + +@ObjectType() +class CurriculumData { + @Field(type => [CurriculumSection]) + sections: CurriculumSection[]; +} + +@ObjectType() +class Curriculum { + @Field() + label: string; + + @Field({ nullable: true }) + data: CurriculumData; +} +@ObjectType() +class BookSection { + @Field() + label: string; + + @Field() + url: string; +} + +@ObjectType() +class BookChapter { + @Field() + label: string; + + @Field({ nullable: true }) + url: string; + + @Field(type => [BookSection], { nullable: true }) + sections: BookSection[]; +} + +@ObjectType() +class BookOnlineData { + @Field(type => [BookChapter]) + chapters: BookChapter[]; +} + +@ObjectType() +class BookOnline { + @Field() + label: string; + + @Field({ nullable: true }) + data: BookOnlineData; +} + +@ObjectType() +class BookDownloadItem { + @Field() + label: string; + + @Field() + description: string; + + @Field() + url: string; + + @Field() + fileName: string; +} + +@ObjectType() +class BookDownloadData { + @Field() + label: string; + + @Field(type => [BookDownloadItem]) + items: BookDownloadItem[]; +} +@ObjectType() +class BookDownload { + @Field() + label: string; + + @Field({ nullable: true }) + data: BookDownloadData; +} + +@ObjectType() +class OnboardingItem { + @Field() + label: string; + + @Field() + url: string; -interface Resolvers { - Query: QueryResolvers; - Mutation: MutationResolvers; -} - -export const resolvers: Resolvers = { - Query: { - unlockedCourses: async (_, __, { me, courseConnector }) => { - if (!me) { - return []; - } - - const courses = await courseConnector.getCoursesByUserId( - me.uid - ); - - const unlockedCourses = mergeCourses(courses); - - return Object.values(unlockedCourses).map(unlockedCourse => ({ - courseId: unlockedCourse.courseId, - header: unlockedCourse.header, - url: unlockedCourse.url, - imageUrl: unlockedCourse.imageUrl, - canUpgrade: unlockedCourse.canUpgrade, - })); - }, - unlockedCourse: async ( - _, - { courseId }, - { me, courseConnector } - ) => { - if (!me) { - return null; - } - - const courses = await courseConnector.getCoursesByUserIdAndCourseId( - me.uid, - courseId - ); - - const unlockedCourses = mergeCourses(courses); - - const unlockedCourse = unlockedCourses.find( - unlockedCourse => unlockedCourse.courseId === courseId - ); - - return unlockedCourse; - }, - }, - Mutation: { - createFreeCourse: async ( - _, - { courseId, bundleId }, - { me, courseConnector } - ) => { - if (!me) { - return false; - } - - await courseConnector.createCourse({ - userId: me.uid, - courseId: courseId, - bundleId: bundleId, - price: 0, - currency: 'USD', - paymentType: 'FREE', - coupon: '', - }); - - // LEGACY - await createCourse({ - uid: me?.uid, - courseId, - bundleId, - amount: 0, - paymentType: 'FREE', - coupon: '', - }); - // LEGACY END - - return true; - }, - createAdminCourse: async ( - _, - { uid, courseId, bundleId }, - { courseConnector } - ) => { - await courseConnector.createCourse({ - userId: uid, - courseId: courseId, - bundleId: bundleId, - price: 0, - currency: 'USD', - paymentType: 'MANUAL', - coupon: '', - }); - - // LEGACY - await createCourse({ - uid, - courseId, - bundleId, - amount: 0, - paymentType: 'MANUAL', - coupon: '', - }); - // LEGACY END - - return true; - }, - }, -}; + @Field() + description: string; + + @Field({ nullable: true }) + secondaryUrl: string; +} +@ObjectType() +class OnboardingData { + @Field(type => [OnboardingItem]) + items: OnboardingItem[]; +} +@ObjectType() +class Onboarding { + @Field() + label: string; + + @Field({ nullable: true }) + data: OnboardingData; +} +@ObjectType() +class IntroductionData { + @Field() + label: string; + + @Field() + url: string; + + @Field() + description: string; +} +@ObjectType() +class Introduction { + @Field() + label: string; + + @Field({ nullable: true }) + data: IntroductionData; +} +@ObjectType() +class UnlockedCourse { + @Field() + courseId: string; + + @Field() + bundleId: string; + + @Field() + header: string; + + @Field() + url: string; + + @Field() + imageUrl: string; + + @Field() + canUpgrade: boolean; + + @Field({ nullable: true }) + introduction: Introduction; + + @Field({ nullable: true }) + onboarding: Onboarding; + + @Field({ nullable: true }) + bookDownload: BookDownload; + + @Field({ nullable: true }) + bookOnline: BookOnline; + + @Field({ nullable: true }) + curriculum: Curriculum; +} + +@Resolver() +export default class CourseResolver { + @Query(() => [StorefrontCourse]) + async unlockedCourses(@Ctx() ctx: ResolverContext) { + if (!ctx.me) { + return []; + } + + const courses = await ctx.courseConnector.getCoursesByUserId( + ctx.me.uid + ); + + const unlockedCourses = mergeCourses(courses); + + return Object.values(unlockedCourses).map(unlockedCourse => ({ + courseId: unlockedCourse.courseId, + header: unlockedCourse.header, + url: unlockedCourse.url, + imageUrl: unlockedCourse.imageUrl, + canUpgrade: unlockedCourse.canUpgrade, + })); + } + + @Query(() => UnlockedCourse, { nullable: true }) + async unlockedCourse( + @Arg('courseId') courseId: string, + @Ctx() ctx: ResolverContext + ) { + if (!ctx.me) { + return null; + } + + const courses = await ctx.courseConnector.getCoursesByUserIdAndCourseId( + ctx.me.uid, + courseId as COURSE + ); + + const unlockedCourses = mergeCourses(courses); + + const unlockedCourse = unlockedCourses.find( + unlockedCourse => unlockedCourse.courseId === courseId + ); + + return unlockedCourse; + } + + @Mutation(() => Boolean) + async createFreeCourse( + @Arg('courseId') courseId: string, + @Arg('bundleId') bundleId: string, + @Ctx() ctx: ResolverContext + ) { + if (!ctx.me) { + return false; + } + + await ctx.courseConnector.createCourse({ + userId: ctx.me.uid, + courseId: courseId as COURSE, + bundleId: bundleId as BUNDLE, + price: 0, + currency: 'USD', + paymentType: 'FREE', + coupon: '', + }); + + // LEGACY + await createCourse({ + uid: ctx.me?.uid, + courseId: courseId as COURSE, + bundleId: bundleId as BUNDLE, + amount: 0, + paymentType: 'FREE', + coupon: '', + }); + // LEGACY END + + return true; + } + + @Mutation(() => Boolean) + async createAdminCourse( + @Arg('courseId') courseId: string, + @Arg('bundleId') bundleId: string, + @Arg('uid') uid: string, + @Ctx() ctx: ResolverContext + ) { + await ctx.courseConnector.createCourse({ + userId: uid, + courseId: courseId as COURSE, + bundleId: bundleId as BUNDLE, + price: 0, + currency: 'USD', + paymentType: 'MANUAL', + coupon: '', + }); + + // LEGACY + await createCourse({ + uid, + courseId: courseId as COURSE, + bundleId: bundleId as BUNDLE, + amount: 0, + paymentType: 'MANUAL', + coupon: '', + }); + // LEGACY END + + return true; + } +} diff --git a/src/api/resolvers/course/spec.ts b/src/api/resolvers/course/spec.ts deleted file mode 100644 index 84585625..00000000 --- a/src/api/resolvers/course/spec.ts +++ /dev/null @@ -1,254 +0,0 @@ -import { createTestClient } from 'apollo-server-testing'; -import { ApolloServer } from 'apollo-server-micro'; - -import firebaseAdmin from '@services/firebase/admin'; -import schema from '@api/schema'; - -import { - CREATE_FREE_COURSE, - CREATE_ADMIN_COURSE, -} from '@queries/course'; - -describe('createFreeCourse', () => { - let set: any; - - beforeEach(() => { - set = firebaseAdmin - .database() - .ref() - .push().set; - }); - - // it('creates a course', async () => { - // const server = new ApolloServer({ - // schema, - // context: () => ({ - // me: { uid: '1', email: 'example@example.com' }, - // }), - // }); - - // const { mutate } = createTestClient(server); - - // const { data, errors } = await mutate({ - // mutation: CREATE_FREE_COURSE, - // variables: { - // courseId: 'THE_ROAD_TO_GRAPHQL', - // bundleId: 'STUDENT', - // }, - // }); - - // expect(data).toMatchSnapshot(); - // expect(errors).toEqual(undefined); - - // expect(set).toHaveBeenCalledTimes(1); - // expect(set).toHaveBeenCalledWith({ - // courseId: 'THE_ROAD_TO_GRAPHQL', - // packageId: 'STUDENT', - // invoice: { - // createdAt: 'TIMESTAMP', - // amount: 0, - // licensesCount: 1, - // currency: 'USD', - // paymentType: 'FREE', - // }, - // }); - // }); - - it('does not create a course if not authenticated', async () => { - const server = new ApolloServer({ - schema, - context: () => ({ - me: null, - }), - }); - - const { mutate } = createTestClient(server); - - const { data, errors } = await mutate({ - mutation: CREATE_FREE_COURSE, - variables: { - courseId: 'THE_ROAD_TO_GRAPHQL', - bundleId: 'STUDENT', - }, - }); - - expect(data).toEqual(null); - expect(errors).toMatchSnapshot(); - expect(set).toHaveBeenCalledTimes(0); - }); - - it('does not create a course if not free', async () => { - const server = new ApolloServer({ - schema, - context: () => ({ - me: null, - }), - }); - - const { mutate } = createTestClient(server); - - const { data, errors } = await mutate({ - mutation: CREATE_FREE_COURSE, - variables: { - courseId: 'THE_ROAD_TO_GRAPHQL', - bundleId: 'PROFESSIONAL', - }, - }); - - expect(data).toEqual(null); - expect(errors).toMatchSnapshot(); - expect(set).toHaveBeenCalledTimes(0); - }); -}); - -describe('createAdminCourse', () => { - let set: any; - let ref: any; - - beforeEach(() => { - ref = firebaseAdmin.database().ref; - - set = firebaseAdmin - .database() - .ref() - .push().set; - }); - - it('creates a course', async () => { - const server = new ApolloServer({ - schema, - context: () => ({ - me: { - uid: '1', - email: 'example@example.com', - customClaims: { admin: true }, - }, - }), - }); - - const { mutate } = createTestClient(server); - - const { data, errors } = await mutate({ - mutation: CREATE_ADMIN_COURSE, - variables: { - uid: '2', - courseId: 'THE_ROAD_TO_GRAPHQL', - bundleId: 'STUDENT', - }, - }); - - expect(data).toMatchSnapshot(); - expect(errors).toEqual(undefined); - - expect(set).toHaveBeenCalledTimes(1); - expect(set).toHaveBeenCalledWith({ - courseId: 'THE_ROAD_TO_GRAPHQL', - packageId: 'STUDENT', - invoice: { - createdAt: 'TIMESTAMP', - amount: 0, - coupon: '', - licensesCount: 1, - currency: 'USD', - paymentType: 'MANUAL', - }, - }); - - expect(ref).toHaveBeenCalledWith('users/2/courses'); - }); - - it('creates a course that is not for free', async () => { - const server = new ApolloServer({ - schema, - context: () => ({ - me: { - uid: '1', - email: 'example@example.com', - customClaims: { admin: true }, - }, - }), - }); - - const { mutate } = createTestClient(server); - - const { data, errors } = await mutate({ - mutation: CREATE_ADMIN_COURSE, - variables: { - uid: '2', - courseId: 'THE_ROAD_TO_GRAPHQL', - bundleId: 'PROFESSIONAL', - }, - }); - - expect(data).toMatchSnapshot(); - expect(errors).toEqual(undefined); - - expect(set).toHaveBeenCalledTimes(1); - expect(set).toHaveBeenCalledWith({ - courseId: 'THE_ROAD_TO_GRAPHQL', - packageId: 'PROFESSIONAL', - invoice: { - createdAt: 'TIMESTAMP', - amount: 0, - coupon: '', - licensesCount: 1, - currency: 'USD', - paymentType: 'MANUAL', - }, - }); - - expect(ref).toHaveBeenCalledWith('users/2/courses'); - }); - - it('does not create a course if not authenticated', async () => { - const server = new ApolloServer({ - schema, - context: () => ({ - me: null, - }), - }); - - const { mutate } = createTestClient(server); - - const { data, errors } = await mutate({ - mutation: CREATE_ADMIN_COURSE, - variables: { - uid: '2', - courseId: 'THE_ROAD_TO_GRAPHQL', - bundleId: 'PROFESSIONAL', - }, - }); - - expect(data).toEqual(null); - expect(errors).toMatchSnapshot(); - expect(set).toHaveBeenCalledTimes(0); - }); - - it('does not create a course if not admin', async () => { - const server = new ApolloServer({ - schema, - context: () => ({ - me: { - uid: '1', - email: 'example@example.com', - customClaims: null, - }, - }), - }); - - const { mutate } = createTestClient(server); - - const { data, errors } = await mutate({ - mutation: CREATE_ADMIN_COURSE, - variables: { - uid: '2', - courseId: 'THE_ROAD_TO_GRAPHQL', - bundleId: 'PROFESSIONAL', - }, - }); - - expect(data).toEqual(null); - expect(errors).toMatchSnapshot(); - expect(set).toHaveBeenCalledTimes(0); - }); -}); diff --git a/src/api/resolvers/index.ts b/src/api/resolvers/index.ts index 16394160..1dbefb7a 100644 --- a/src/api/resolvers/index.ts +++ b/src/api/resolvers/index.ts @@ -1,33 +1,27 @@ -import { GraphQLDateTime } from 'graphql-iso-date'; -import { resolvers as migrationResolvers } from './migration'; -import { resolvers as sessionResolvers } from './session'; -import { resolvers as userResolvers } from './user'; -import { resolvers as storefrontResolvers } from './storefront'; -import { resolvers as paypalResolvers } from './paypal'; -import { resolvers as stripeResolvers } from './stripe'; -import { resolvers as courseResolvers } from './course'; -import { resolvers as bookResolvers } from './book'; -import { resolvers as upgradeResolvers } from './upgrade'; -import { resolvers as couponResolvers } from './coupon'; -import { resolvers as partnerResolvers } from './partner'; -import { resolvers as communityResolvers } from './community'; - -const customScalarResolver = { - DateTime: GraphQLDateTime, -}; +import MigrationResolvers from './migration'; +import SessionResolver from './session'; +import UserResolvers from './user'; +import StorefrontResolvers from './storefront'; +import PaypalResolvers from './paypal'; +import StripeResolvers from './stripe'; +import CourseResolvers from './course'; +import BookResolvers from './book'; +import UpgradeResolvers from './upgrade'; +import CouponResolver from './coupon'; +import PartnerResolver from './partner'; +import CommunityResolvers from './community'; export default [ - customScalarResolver, - migrationResolvers, - sessionResolvers, - userResolvers, - storefrontResolvers, - paypalResolvers, - stripeResolvers, - courseResolvers, - bookResolvers, - upgradeResolvers, - couponResolvers, - partnerResolvers, - communityResolvers, + MigrationResolvers, + SessionResolver, + UserResolvers, + StorefrontResolvers, + PaypalResolvers, + StripeResolvers, + CourseResolvers, + BookResolvers, + UpgradeResolvers, + CouponResolver, + PartnerResolver, + CommunityResolvers, ]; diff --git a/src/api/resolvers/migration/index.ts b/src/api/resolvers/migration/index.ts index 9b4283c3..5d57bfcf 100644 --- a/src/api/resolvers/migration/index.ts +++ b/src/api/resolvers/migration/index.ts @@ -1,20 +1,16 @@ -import { MutationResolvers } from '@generated/server'; +import { Arg, Resolver, Mutation } from 'type-graphql'; -interface Resolvers { - Mutation: MutationResolvers; +@Resolver() +export default class MigrationResolver { + @Mutation(() => Boolean) + async migrate(@Arg('migrationType') migrationType: string) { + switch (migrationType) { + case 'FOO': + return true; + case 'BAR': + return true; + default: + return false; + } + } } - -export const resolvers: Resolvers = { - Mutation: { - migrate: async (_, { migrationType }) => { - switch (migrationType) { - case 'FOO': - return true; - case 'BAR': - return true; - default: - return false; - } - }, - }, -}; diff --git a/src/api/resolvers/partner/index.ts b/src/api/resolvers/partner/index.ts index 71ad951f..eb4f7b41 100644 --- a/src/api/resolvers/partner/index.ts +++ b/src/api/resolvers/partner/index.ts @@ -1,110 +1,181 @@ -import { QueryResolvers, MutationResolvers } from '@generated/server'; +import { + ObjectType, + Field, + Arg, + Ctx, + Resolver, + Query, + Mutation, +} from 'type-graphql'; + +import { ResolverContext } from '@typeDefs/resolver'; import { hasPartnerRole } from '@validation/partner'; -interface Resolvers { - Query: QueryResolvers; - Mutation: MutationResolvers; +@ObjectType() +export class VisitorByDay { + @Field() + date: Date; + + @Field() + count: number; } -export const resolvers: Resolvers = { - Query: { - partnerVisitors: async ( - _, - { from, to }, - { partnerConnector } - ) => { - try { - return await partnerConnector.getVisitorsBetweenAggregatedByDate( - from, - to - ); - } catch (error) { - return []; - } - }, - partnerSales: async ( - _, - { offset, limit }, - { me, partnerConnector } - ) => { - if (!me) { - return []; - } +@ObjectType() +class PartnerSale { + @Field() + id: string; + + @Field() + createdAt: Date; + + @Field() + royalty: number; + + @Field() + price: number; + + @Field() + courseId: string; + + @Field() + bundleId: string; + + @Field() + isCoupon: boolean; +} + +@ObjectType() +class PageInfo { + @Field() + total: number; +} + +@ObjectType() +class PartnerSaleConnection { + @Field(type => [PartnerSale]) + edges: PartnerSale[]; + + @Field() + pageInfo: PageInfo; +} - try { - const { - edges, +@ObjectType() +export class PartnerPayment { + @Field() + createdAt: Date; + + @Field() + royalty: number; +} +@Resolver() +export default class PartnerResolver { + @Query(() => [VisitorByDay]) + async partnerVisitors( + @Arg('from') from: Date, + @Arg('to') to: Date, + @Ctx() ctx: ResolverContext + ) { + try { + return await ctx.partnerConnector.getVisitorsBetweenAggregatedByDate( + from, + to + ); + } catch (error) { + return []; + } + } + + @Query(() => PartnerSaleConnection) + async partnerSales( + @Arg('offset') offset: number, + @Arg('limit') limit: number, + @Ctx() ctx: ResolverContext + ) { + if (!ctx.me) { + return []; + } + + try { + const { + edges, + total, + } = await ctx.partnerConnector.getSalesByPartner( + ctx.me.uid, + offset, + limit + ); + + return { + edges: edges.map(saleByPartner => ({ + id: saleByPartner.id, + createdAt: saleByPartner.createdAt, + royalty: saleByPartner.royalty, + price: saleByPartner.course.price, + courseId: saleByPartner.course.courseId, + bundleId: saleByPartner.course.bundleId, + isCoupon: !!saleByPartner.course.coupon, + })), + pageInfo: { total, - } = await partnerConnector.getSalesByPartner( - me.uid, - offset, - limit - ); - - return { - edges: edges.map(saleByPartner => ({ - id: saleByPartner.id, - createdAt: saleByPartner.createdAt, - royalty: saleByPartner.royalty, - price: saleByPartner.course.price, - courseId: saleByPartner.course.courseId, - bundleId: saleByPartner.course.bundleId, - isCoupon: !!saleByPartner.course.coupon, - })), - pageInfo: { - total, - }, - }; - } catch (error) { - return []; - } - }, - partnerPayments: async (_, __, { me, partnerConnector }) => { - if (!me) { - return []; - } + }, + }; + } catch (error) { + return []; + } + } - try { - return await partnerConnector.getPaymentsByPartner(me.uid); - } catch (error) { - return []; - } - }, - }, - Mutation: { - promoteToPartner: async (_, { uid }, { adminConnector }) => { - try { - await adminConnector.setCustomClaims(uid, { - partner: true, - }); - } catch (error) { - throw new Error(error); - } + @Query(() => [PartnerPayment]) + async partnerPayments(@Ctx() ctx: ResolverContext) { + if (!ctx.me) { + return []; + } - return true; - }, - partnerTrackVisitor: async ( - _, - { partnerId }, - { partnerConnector, adminConnector } - ) => { - try { - const partner = await adminConnector.getUser(partnerId); - - if (!hasPartnerRole(partner)) { - return false; - } - } catch (error) { - return false; - } + try { + return await ctx.partnerConnector.getPaymentsByPartner( + ctx.me.uid + ); + } catch (error) { + return []; + } + } + + @Mutation(() => Boolean) + async promoteToPartner( + @Arg('uid') uid: string, + @Ctx() ctx: ResolverContext + ) { + try { + await ctx.adminConnector.setCustomClaims(uid, { + partner: true, + }); + } catch (error) { + throw new Error(error); + } - try { - await partnerConnector.createVisitor(partnerId); - } catch (error) { + return true; + } + + @Mutation(() => Boolean) + async partnerTrackVisitor( + @Arg('partnerId') partnerId: string, + @Ctx() ctx: ResolverContext + ) { + try { + const partner = await ctx.adminConnector.getUser(partnerId); + + if (!hasPartnerRole(partner)) { return false; } + } catch (error) { + return false; + } - return true; - }, - }, -}; + try { + await ctx.partnerConnector.createVisitor(partnerId); + } catch (error) { + return false; + } + + return true; + } +} diff --git a/src/api/resolvers/paypal/index.ts b/src/api/resolvers/paypal/index.ts index 50f8df19..41f758c3 100644 --- a/src/api/resolvers/paypal/index.ts +++ b/src/api/resolvers/paypal/index.ts @@ -1,124 +1,152 @@ +import { + ObjectType, + Field, + Arg, + Ctx, + Resolver, + Mutation, +} from 'type-graphql'; + +import { COURSE } from '@data/course-keys-types'; +import { BUNDLE } from '@data/bundle-keys-types'; +import { ResolverContext } from '@typeDefs/resolver'; + // TODO https://github.com/paypal/Checkout-NodeJS-SDK/issues/25 import paypal from '@paypal/checkout-server-sdk'; -import { MutationResolvers } from '@generated/server'; import { priceWithDiscount } from '@services/discount'; import paypalClient from '@services/paypal'; import { createCourse } from '@services/firebase/course'; import storefront from '@data/course-storefront'; -interface Resolvers { - Mutation: MutationResolvers; +@ObjectType() +class PaypalOrderId { + @Field({ nullable: true }) + orderId: string | undefined | null; } -export const resolvers: Resolvers = { - Mutation: { - // https://developer.paypal.com/docs/checkout/reference/server-integration/set-up-transaction/ - paypalCreateOrder: async ( - _, - { courseId, bundleId, coupon, partnerId }, - { me, couponConnector, courseConnector } - ) => { - const course = storefront[courseId]; - const bundle = course.bundles[bundleId]; - - if (!me) { - return { orderId: null }; - } - - const price = await priceWithDiscount( - couponConnector, - courseConnector - )(courseId, bundleId, bundle.price, coupon, me?.uid); - - const request = new paypal.orders.OrdersCreateRequest(); - - request.prefer('return=representation'); - - request.requestBody({ - intent: 'CAPTURE', - purchase_units: [ - { - reference_id: courseId, - custom_id: JSON.stringify({ - courseId, - bundleId, - coupon, - partnerId, - }), - description: `${courseId} ${bundleId}`, - amount: { - currency_code: 'USD', - value: (price / 100).toFixed(2), - }, +@Resolver() +export default class PaypalResolver { + // https://developer.paypal.com/docs/checkout/reference/server-integration/set-up-transaction/ + @Mutation(() => PaypalOrderId) + async paypalCreateOrder( + @Arg('courseId') courseId: string, + @Arg('bundleId') bundleId: string, + + @Arg('coupon', { nullable: true }) + coupon: string | undefined | null, + + @Arg('partnerId', { nullable: true }) + partnerId: string | undefined | null, + + @Ctx() ctx: ResolverContext + ): Promise { + const course = storefront[courseId as COURSE]; + const bundle = course.bundles[bundleId as BUNDLE]; + + if (!ctx.me) { + return { orderId: null }; + } + + const price = await priceWithDiscount( + ctx.couponConnector, + ctx.courseConnector + )( + courseId as COURSE, + bundleId as BUNDLE, + bundle.price, + coupon, + ctx.me?.uid + ); + + const request = new paypal.orders.OrdersCreateRequest(); + + request.prefer('return=representation'); + + request.requestBody({ + intent: 'CAPTURE', + purchase_units: [ + { + reference_id: courseId, + custom_id: JSON.stringify({ + courseId, + bundleId, + coupon, + partnerId, + }), + description: `${courseId} ${bundleId}`, + amount: { + currency_code: 'USD', + value: (price / 100).toFixed(2), }, - ], + }, + ], + }); + + let order; + try { + order = await paypalClient().execute(request); + } catch (error) { + throw new Error(error); + } + + return { orderId: order.result.id }; + } + + // https://developer.paypal.com/docs/checkout/reference/server-integration/capture-transaction/ + @Mutation(() => Boolean) + async paypalApproveOrder( + @Arg('orderId') orderId: string, + @Ctx() ctx: ResolverContext + ): Promise { + const request = new paypal.orders.OrdersCaptureRequest(orderId); + request.requestBody({}); + + try { + const capture = await paypalClient().execute(request); + + const { + amount, + custom_id, + } = capture.result.purchase_units[0].payments.captures[0]; + + const { courseId, bundleId, coupon, partnerId } = JSON.parse( + custom_id + ); + + const course = await ctx.courseConnector.createCourse({ + userId: ctx.me!.uid, + courseId: courseId, + bundleId: bundleId, + price: +amount.value.replace('.', ''), + currency: 'USD', + paymentType: 'PAYPAL', + coupon: coupon, }); - let order; - try { - order = await paypalClient().execute(request); - } catch (error) { - throw new Error(error); + if (coupon) { + await ctx.couponConnector.removeCoupon(coupon); } - return { orderId: order.result.id }; - }, - // https://developer.paypal.com/docs/checkout/reference/server-integration/capture-transaction/ - paypalApproveOrder: async ( - _, - { orderId }, - { me, courseConnector, partnerConnector, couponConnector } - ) => { - const request = new paypal.orders.OrdersCaptureRequest(orderId); - request.requestBody({}); - - try { - const capture = await paypalClient().execute(request); - - const { - amount, - custom_id, - } = capture.result.purchase_units[0].payments.captures[0]; - - const { courseId, bundleId, coupon, partnerId } = JSON.parse( - custom_id - ); - - const course = await courseConnector.createCourse({ - userId: me!.uid, - courseId: courseId, - bundleId: bundleId, - price: +amount.value.replace('.', ''), - currency: 'USD', - paymentType: 'PAYPAL', - coupon: coupon, - }); - - if (coupon) { - await couponConnector.removeCoupon(coupon); - } - - if (partnerId && partnerId !== me?.uid) { - await partnerConnector.createSale(course, partnerId); - } - - // LEGACY - await createCourse({ - uid: me?.uid, - courseId, - bundleId, - amount: amount.value, - paymentType: 'PAYPAL', - coupon, - }); - // LEGACY END - } catch (error) { - throw new Error(error); + if (partnerId && partnerId !== ctx.me?.uid) { + await ctx.partnerConnector.createSale(course, partnerId); } - return true; - }, - }, -}; + // LEGACY + await createCourse({ + uid: ctx.me?.uid, + courseId, + bundleId, + amount: amount.value, + paymentType: 'PAYPAL', + coupon, + }); + // LEGACY END + } catch (error) { + throw new Error(error); + } + + return true; + } +} diff --git a/src/api/resolvers/session/index.ts b/src/api/resolvers/session/index.ts index 3a335fb2..7f1c0937 100644 --- a/src/api/resolvers/session/index.ts +++ b/src/api/resolvers/session/index.ts @@ -1,118 +1,146 @@ -import { MutationResolvers } from '@generated/server'; +import { + ObjectType, + Field, + Arg, + Ctx, + Resolver, + Mutation, +} from 'type-graphql'; + +import { ResolverContext } from '@typeDefs/resolver'; import { EXPIRES_IN } from '@constants/cookie'; import firebase from '@services/firebase/client'; import firebaseAdmin from '@services/firebase/admin'; import { inviteToRevue } from '@services/revue'; import { inviteToConvertkit } from '@services/convertkit'; -interface Resolvers { - Mutation: MutationResolvers; +@ObjectType() +class SessionToken { + @Field() + sessionToken: string; } -export const resolvers: Resolvers = { - Mutation: { - signIn: async (_, { email, password }) => { - let result; +@Resolver() +export default class SessionResolver { + @Mutation(() => SessionToken) + async signIn( + @Arg('email') email: string, + @Arg('password') password: string + ) { + let result; + + try { + result = await firebase + .auth() + .signInWithEmailAndPassword(email, password); + } catch (error) { + return new Error(error); + } + + const idToken = await result.user?.getIdToken(); + const sessionToken = await firebaseAdmin + .auth() + .createSessionCookie(idToken || '', { + expiresIn: EXPIRES_IN, + }); + + // We manage the session ourselves. + await firebase.auth().signOut(); - try { - result = await firebase - .auth() - .signInWithEmailAndPassword(email, password); - } catch (error) { + return { sessionToken }; + } + + @Mutation(() => SessionToken) + async signUp( + @Arg('username') username: string, + @Arg('email') email: string, + @Arg('password') password: string + ) { + try { + await firebaseAdmin.auth().createUser({ + email, + password, + displayName: username, + }); + } catch (error) { + if (error.message.includes('email address is already in use')) { + return new Error( + 'You already registered with this email. Hint: Check your password manager for our old domain: roadtoreact.com' + ); + } else { return new Error(error); } + } - const idToken = await result.user?.getIdToken(); - const sessionToken = await firebaseAdmin - .auth() - .createSessionCookie(idToken || '', { - expiresIn: EXPIRES_IN, - }); - - // We manage the session ourselves. - await firebase.auth().signOut(); - - return { sessionToken }; - }, - signUp: async (_, { username, email, password }) => { - try { - await firebaseAdmin.auth().createUser({ - email, - password, - displayName: username, - }); - } catch (error) { - if ( - error.message.includes('email address is already in use') - ) { - const customError = - 'You already registered with this email. Hint: Check your password manager for our old domain: roadtoreact.com'; - return new Error(customError); - } else { - return new Error(error); - } - } + const { user } = await firebase + .auth() + .signInWithEmailAndPassword(email, password); - const { - user, - } = await firebase - .auth() - .signInWithEmailAndPassword(email, password); + const idToken = await user?.getIdToken(); + const sessionToken = await firebaseAdmin + .auth() + .createSessionCookie(idToken || '', { + expiresIn: EXPIRES_IN, + }); - const idToken = await user?.getIdToken(); - const sessionToken = await firebaseAdmin - .auth() - .createSessionCookie(idToken || '', { - expiresIn: EXPIRES_IN, - }); + // We manage the session ourselves. + await firebase.auth().signOut(); - // We manage the session ourselves. - await firebase.auth().signOut(); + try { + inviteToConvertkit(email, username); + } catch (error) { + console.log(error); + } - try { - inviteToConvertkit(email, username); - } catch (error) { - console.log(error); - } + try { + inviteToRevue(email, username); + } catch (error) { + console.log(error); + } - try { - inviteToRevue(email, username); - } catch (error) { - console.log(error); - } + return { sessionToken }; + } - return { sessionToken }; - }, - passwordForgot: async (_, { email }) => { - try { - await firebase.auth().sendPasswordResetEmail(email); - } catch (error) { - return new Error(error); - } + @Mutation(() => Boolean, { nullable: true }) + async passwordForgot(@Arg('email') email: string) { + try { + await firebase.auth().sendPasswordResetEmail(email); + } catch (error) { + return new Error(error); + } - return true; - }, - passwordChange: async (_, { password }, { me }) => { - try { - await firebaseAdmin.auth().updateUser(me?.uid || '', { - password, - }); - } catch (error) { - return new Error(error); - } + return true; + } - return true; - }, - emailChange: async (_, { email }, { me }) => { - try { - await firebaseAdmin.auth().updateUser(me?.uid || '', { - email, - }); - } catch (error) { - return new Error(error); - } + @Mutation(() => Boolean, { nullable: true }) + async passwordChange( + @Arg('password') password: string, + @Ctx() ctx: ResolverContext + ) { + try { + await firebaseAdmin.auth().updateUser(ctx.me?.uid || '', { + password, + }); + } catch (error) { + return new Error(error); + } + + return true; + } - return true; - }, - }, -}; + @Mutation(() => Boolean, { nullable: true }) + async emailChange( + @Arg('email') email: string, + @Ctx() ctx: ResolverContext + ) { + try { + await firebaseAdmin.auth().updateUser(ctx.me?.uid || '', { + email, + }); + } catch (error) { + return new Error(error); + } + + return true; + } +} diff --git a/src/api/resolvers/storefront/__snapshots__/spec.ts.snap b/src/api/resolvers/storefront/__snapshots__/spec.ts.snap deleted file mode 100644 index 5b764fe7..00000000 --- a/src/api/resolvers/storefront/__snapshots__/spec.ts.snap +++ /dev/null @@ -1,22 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`storefront returns nothing if no user is signed in 1`] = ` -Object { - "me": null, -} -`; - -exports[`storefront returns nothing if no user is signed in 2`] = ` -Array [ - [GraphQLError: Not authenticated as user.], -] -`; - -exports[`storefront returns the signed in user 1`] = ` -Object { - "me": Object { - "email": "example@example.com", - "uid": "1", - }, -} -`; diff --git a/src/api/resolvers/storefront/index.ts b/src/api/resolvers/storefront/index.ts index 4edcd9f5..c1fceb4b 100644 --- a/src/api/resolvers/storefront/index.ts +++ b/src/api/resolvers/storefront/index.ts @@ -1,44 +1,96 @@ +import { + ObjectType, + Field, + Arg, + Resolver, + Query, +} from 'type-graphql'; + +import { COURSE } from '@data/course-keys-types'; +import { BUNDLE } from '@data/bundle-keys-types'; + import sortBy from 'lodash.sortby'; -import { QueryResolvers } from '@generated/server'; import storefront from '@data/course-storefront'; -interface Resolvers { - Query: QueryResolvers; +@ObjectType() +export class StorefrontBundle { + @Field() + header: string; + + @Field() + bundleId: string; + + @Field() + price: number; + + @Field() + imageUrl: string; + + @Field(type => [String]) + benefits: string[]; +} + +@ObjectType() +export class StorefrontCourse { + @Field() + header: string; + + @Field() + courseId: string; + + @Field() + url: string; + + @Field() + imageUrl: string; + + @Field() + canUpgrade: boolean; + + @Field() + bundle: StorefrontBundle; } -export const resolvers: Resolvers = { - Query: { - storefrontCourse: (_, { courseId, bundleId }) => { - const course = storefront[courseId]; - const bundle = course.bundles[bundleId]; - - return { - ...course, - header: course.header, - courseId: course.courseId, - url: course.url, - imageUrl: course.imageUrl, - canUpgrade: false, - bundle, - }; - }, - storefrontCourses: () => { - return Object.values(storefront).map(storefrontCourse => ({ - courseId: storefrontCourse.courseId, - header: storefrontCourse.header, - url: storefrontCourse.url, - imageUrl: storefrontCourse.imageUrl, - canUpgrade: false, - })); - }, - storefrontBundles: (_, { courseId }) => { - const course = storefront[courseId]; - - return sortBy( - Object.values(course.bundles), - (bundle: any) => bundle.weight - ); - }, - }, -}; +@Resolver() +export default class StorefrontResolver { + @Query(() => StorefrontCourse) + async storefrontCourse( + @Arg('courseId') courseId: string, + @Arg('bundleId') bundleId: string + ) { + const course = storefront[courseId as COURSE]; + const bundle = course.bundles[bundleId as BUNDLE]; + + return { + ...course, + header: course.header, + courseId: course.courseId, + url: course.url, + imageUrl: course.imageUrl, + canUpgrade: false, + bundle, + }; + } + + @Query(() => [StorefrontCourse]) + async storefrontCourses() { + return Object.values(storefront).map(storefrontCourse => ({ + courseId: storefrontCourse.courseId, + header: storefrontCourse.header, + url: storefrontCourse.url, + imageUrl: storefrontCourse.imageUrl, + canUpgrade: false, + })); + } + + @Query(() => [StorefrontBundle]) + async storefrontBundles(@Arg('courseId') courseId: string) { + const course = storefront[courseId as COURSE]; + + return sortBy( + Object.values(course.bundles), + (bundle: any) => bundle.weight + ); + } +} diff --git a/src/api/resolvers/storefront/spec.ts b/src/api/resolvers/storefront/spec.ts deleted file mode 100644 index a1d994a8..00000000 --- a/src/api/resolvers/storefront/spec.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { createTestClient } from 'apollo-server-testing'; -import { ApolloServer } from 'apollo-server-micro'; - -import schema from '@api/schema'; - -import { GET_ME } from '@queries/user'; - -describe('storefront', () => { - it('returns the signed in user', async () => { - const server = new ApolloServer({ - schema, - context: () => ({ - me: { uid: '1', email: 'example@example.com' }, - }), - }); - - const { mutate } = createTestClient(server); - - const { data, errors } = await mutate({ - mutation: GET_ME, - }); - - expect(data).toMatchSnapshot(); - expect(errors).toEqual(undefined); - }); - - it('returns nothing if no user is signed in', async () => { - const server = new ApolloServer({ - schema, - context: () => ({ - me: null, - }), - }); - - const { mutate } = createTestClient(server); - - const { data, errors } = await mutate({ - mutation: GET_ME, - }); - - expect(data).toMatchSnapshot(); - expect(errors).toMatchSnapshot(); - }); -}); diff --git a/src/api/resolvers/stripe/index.ts b/src/api/resolvers/stripe/index.ts index 331169b8..b6978b02 100644 --- a/src/api/resolvers/stripe/index.ts +++ b/src/api/resolvers/stripe/index.ts @@ -1,67 +1,95 @@ -import { MutationResolvers } from '@generated/server'; +import { + ObjectType, + Field, + Arg, + Ctx, + Resolver, + Mutation, +} from 'type-graphql'; + +import { COURSE } from '@data/course-keys-types'; +import { BUNDLE } from '@data/bundle-keys-types'; +import { ResolverContext } from '@typeDefs/resolver'; + +@ObjectType() +class StripeId { + @Field({ nullable: true }) + id: string | undefined | null; +} + import { priceWithDiscount } from '@services/discount'; import stripe from '@services/stripe'; import storefront from '@data/course-storefront'; -interface Resolvers { - Mutation: MutationResolvers; -} +// https://stripe.com/docs/payments/checkout/one-time#create-one-time-payments +@Resolver() +export default class StripeResolver { + @Mutation(() => StripeId) + async stripeCreateOrder( + @Arg('imageUrl') imageUrl: string, + @Arg('courseId') courseId: string, + @Arg('bundleId') bundleId: string, -export const resolvers: Resolvers = { - Mutation: { - // https://stripe.com/docs/payments/checkout/one-time#create-one-time-payments - stripeCreateOrder: async ( - _, - { imageUrl, courseId, bundleId, coupon, partnerId }, - { me, couponConnector, courseConnector } - ) => { - const course = storefront[courseId]; - const bundle = course.bundles[bundleId]; + @Arg('coupon', { nullable: true }) + coupon: string | undefined | null, - if (!me) { - return { id: null }; - } + @Arg('partnerId', { nullable: true }) + partnerId: string | undefined | null, - const price = await priceWithDiscount( - couponConnector, - courseConnector - )(courseId, bundleId, bundle.price, coupon, me.uid); + @Ctx() ctx: ResolverContext + ): Promise { + const course = storefront[courseId as COURSE]; + const bundle = course.bundles[bundleId as BUNDLE]; - let session; + if (!ctx.me) { + return { id: null }; + } - try { - session = await stripe.checkout.sessions.create({ - customer_email: me?.email, - client_reference_id: me?.uid, - payment_method_types: ['card'], - line_items: [ - { - name: course.header, - description: bundle.header, - images: [imageUrl], - amount: price, - currency: 'usd', - quantity: 1, - }, - ], - metadata: { - courseId, - bundleId, - coupon, - partnerId, - }, - payment_intent_data: { - description: `${courseId} ${bundleId}`, + const price = await priceWithDiscount( + ctx.couponConnector, + ctx.courseConnector + )( + courseId as COURSE, + bundleId as BUNDLE, + bundle.price, + coupon, + ctx.me.uid + ); + + let session; + + try { + session = await stripe.checkout.sessions.create({ + customer_email: ctx.me?.email, + client_reference_id: ctx.me?.uid, + payment_method_types: ['card'], + line_items: [ + { + name: course.header, + description: bundle.header, + images: [imageUrl], + amount: price, + currency: 'usd', + quantity: 1, }, - success_url: process.env.BASE_URL, - cancel_url: `${process.env.BASE_URL}/checkout?courseId=${courseId}&bundleId=${bundleId}`, - }); - } catch (error) { - throw new Error(error); - } + ], + metadata: { + courseId, + bundleId, + coupon, + partnerId, + }, + payment_intent_data: { + description: `${courseId} ${bundleId}`, + }, + success_url: process.env.BASE_URL, + cancel_url: `${process.env.BASE_URL}/checkout?courseId=${courseId}&bundleId=${bundleId}`, + }); + } catch (error) { + throw new Error(error); + } - return { id: session.id }; - }, - }, -}; + return { id: session.id }; + } +} diff --git a/src/api/resolvers/upgrade/index.ts b/src/api/resolvers/upgrade/index.ts index bdcecc4e..5c4e51d8 100644 --- a/src/api/resolvers/upgrade/index.ts +++ b/src/api/resolvers/upgrade/index.ts @@ -1,27 +1,26 @@ -import { QueryResolvers } from '@generated/server'; -import { getUpgradeableCourses } from '@services/course'; +import { Arg, Ctx, Resolver, Query } from 'type-graphql'; -interface Resolvers { - Query: QueryResolvers; -} +import { StorefrontCourse } from '@api/resolvers/storefront'; +import { ResolverContext } from '@typeDefs/resolver'; +import { getUpgradeableCourses } from '@services/course'; +import { COURSE } from '@data/course-keys-types'; -export const resolvers: Resolvers = { - Query: { - upgradeableCourses: async ( - _, - { courseId }, - { me, courseConnector } - ) => { - if (!me) { - return []; - } +@Resolver() +export default class UpgradeResolver { + @Query(() => [StorefrontCourse]) + async upgradeableCourses( + @Arg('courseId') courseId: string, + @Ctx() ctx: ResolverContext + ) { + if (!ctx.me) { + return []; + } - const courses = await courseConnector.getCoursesByUserIdAndCourseId( - me.uid, - courseId - ); + const courses = await ctx.courseConnector.getCoursesByUserIdAndCourseId( + ctx.me.uid, + courseId as COURSE + ); - return getUpgradeableCourses(courseId, courses); - }, - }, -}; + return getUpgradeableCourses(courseId as COURSE, courses); + } +} diff --git a/src/api/resolvers/user/__snapshots__/spec.ts.snap b/src/api/resolvers/user/__snapshots__/spec.ts.snap deleted file mode 100644 index c19b712a..00000000 --- a/src/api/resolvers/user/__snapshots__/spec.ts.snap +++ /dev/null @@ -1,22 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`me returns nothing if no user is signed in 1`] = ` -Object { - "me": null, -} -`; - -exports[`me returns nothing if no user is signed in 2`] = ` -Array [ - [GraphQLError: Not authenticated as user.], -] -`; - -exports[`me returns the signed in user 1`] = ` -Object { - "me": Object { - "email": "example@example.com", - "uid": "1", - }, -} -`; diff --git a/src/api/resolvers/user/index.ts b/src/api/resolvers/user/index.ts index d3501914..6794fda9 100644 --- a/src/api/resolvers/user/index.ts +++ b/src/api/resolvers/user/index.ts @@ -1,23 +1,42 @@ -import { QueryResolvers } from '@generated/server'; +import { + ObjectType, + Field, + Ctx, + Resolver, + Query, +} from 'type-graphql'; -interface Resolvers { - Query: QueryResolvers; +import { ResolverContext } from '@typeDefs/resolver'; + +@ObjectType() +class User { + @Field() + email: string; + + @Field() + uid: string; + + @Field() + username: string; + + @Field(type => [String]) + roles: string[]; } -export const resolvers: Resolvers = { - Query: { - me: (_, __, { me }) => { - const rolesObject = me?.customClaims || {}; - const roles = Object.keys(rolesObject).filter( - key => rolesObject[key] - ); +@Resolver() +export default class UserResolver { + @Query(() => User) + async me(@Ctx() ctx: ResolverContext) { + const rolesObject = ctx.me?.customClaims || {}; + const roles = Object.keys(rolesObject).filter( + key => rolesObject[key] + ); - return { - email: me?.email, - uid: me?.uid, - username: me?.displayName, - roles, - }; - }, - }, -}; + return { + email: ctx.me?.email, + uid: ctx.me?.uid, + username: ctx.me?.displayName, + roles, + }; + } +} diff --git a/src/api/resolvers/user/spec.ts b/src/api/resolvers/user/spec.ts deleted file mode 100644 index 42a60997..00000000 --- a/src/api/resolvers/user/spec.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { createTestClient } from 'apollo-server-testing'; -import { ApolloServer } from 'apollo-server-micro'; - -import schema from '@api/schema'; - -import { GET_ME } from '@queries/user'; - -describe('me', () => { - it('returns the signed in user', async () => { - const server = new ApolloServer({ - schema, - context: () => ({ - me: { uid: '1', email: 'example@example.com' }, - }), - }); - - const { mutate } = createTestClient(server); - - const { data, errors } = await mutate({ - mutation: GET_ME, - }); - - expect(data).toMatchSnapshot(); - expect(errors).toEqual(undefined); - }); - - it('returns nothing if no user is signed in', async () => { - const server = new ApolloServer({ - schema, - context: () => ({ - me: null, - }), - }); - - const { mutate } = createTestClient(server); - - const { data, errors } = await mutate({ - mutation: GET_ME, - }); - - expect(data).toMatchSnapshot(); - expect(errors).toMatchSnapshot(); - }); -}); diff --git a/src/api/schema/index.ts b/src/api/schema/index.ts index 0c9eb83a..b83516c8 100644 --- a/src/api/schema/index.ts +++ b/src/api/schema/index.ts @@ -1,46 +1,46 @@ -import { mergeSchemas } from 'graphql-tools'; -import { applyMiddleware } from 'graphql-middleware'; -import { sentry } from 'graphql-middleware-sentry'; -import * as Sentry from '@sentry/node'; - -import { ResolverContext } from '@typeDefs/resolver'; - -Sentry.init({ - dsn: process.env.SENTRY_DSN, -}); - -const sentryMiddleware = sentry({ - sentryInstance: Sentry, - config: { - environment: process.env.NODE_ENV, - }, - forwardErrors: true, - captureReturnedErrors: true, - withScope: (scope, error, context: ResolverContext) => { - scope.setUser({ - id: context.me?.uid, - email: context.me?.email, - }); - - scope.setExtra('body', context.req.body); - scope.setExtra('origin', context.req.headers.origin); - scope.setExtra('user-agent', context.req.headers['user-agent']); - }, -}); - -import { Resolvers } from '@generated/server'; - -import authorization from '@api/authorization'; -import typeDefs from '@api/typeDefs'; -import resolvers from '@api/resolvers'; - -const schema = mergeSchemas({ - schemas: typeDefs, - resolvers: resolvers as Resolvers, -}); - -export default applyMiddleware( - schema, - authorization, - sentryMiddleware -); +// import { mergeSchemas } from 'graphql-tools'; +// import { applyMiddleware } from 'graphql-middleware'; +// import { sentry } from 'graphql-middleware-sentry'; +// import * as Sentry from '@sentry/node'; + +// import { ResolverContext } from '@typeDefs/resolver'; + +// Sentry.init({ +// dsn: process.env.SENTRY_DSN, +// }); + +// const sentryMiddleware = sentry({ +// sentryInstance: Sentry, +// config: { +// environment: process.env.NODE_ENV, +// }, +// forwardErrors: true, +// captureReturnedErrors: true, +// withScope: (scope, error, context: ResolverContext) => { +// scope.setUser({ +// id: context.me?.uid, +// email: context.me?.email, +// }); + +// scope.setExtra('body', context.req.body); +// scope.setExtra('origin', context.req.headers.origin); +// scope.setExtra('user-agent', context.req.headers['user-agent']); +// }, +// }); + +// import { Resolvers } from '@generated/server'; + +// import authorization from '@api/authorization'; +// import typeDefs from '@api/typeDefs'; +// import resolvers from '@api/resolvers'; + +// const schema = mergeSchemas({ +// schemas: typeDefs, +// resolvers: resolvers as Resolvers, +// }); + +// export default applyMiddleware( +// schema, +// authorization, +// sentryMiddleware +// ); diff --git a/src/api/typeDefs/book/index.ts b/src/api/typeDefs/book/index.ts deleted file mode 100644 index 1d553fb9..00000000 --- a/src/api/typeDefs/book/index.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { gql } from 'apollo-server-micro'; - -export default gql` - extend type Query { - book(path: String!, fileName: String!): File! - - onlineChapter(path: String!): Markdown! - } - - type File { - fileName: String! - contentType: String! - body: String! - } - - type Markdown { - body: String! - } -`; diff --git a/src/api/typeDefs/community/index.ts b/src/api/typeDefs/community/index.ts deleted file mode 100644 index 231416b2..00000000 --- a/src/api/typeDefs/community/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { gql } from 'apollo-server-micro'; - -export default gql` - extend type Mutation { - communityJoin(email: String!): Boolean - } -`; diff --git a/src/api/typeDefs/coupon/index.ts b/src/api/typeDefs/coupon/index.ts deleted file mode 100644 index c7baccbb..00000000 --- a/src/api/typeDefs/coupon/index.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { gql } from 'apollo-server-micro'; - -export default gql` - extend type Query { - discountedPrice( - courseId: CourseId! - bundleId: BundleId! - coupon: String! - ): Discount! - } - - extend type Mutation { - couponCreate( - coupon: String! - discount: Int! - count: Int! - ): Boolean - } - - type Discount { - price: Int! - isDiscount: Boolean! - } -`; diff --git a/src/api/typeDefs/course/index.ts b/src/api/typeDefs/course/index.ts deleted file mode 100644 index 09c98ebd..00000000 --- a/src/api/typeDefs/course/index.ts +++ /dev/null @@ -1,127 +0,0 @@ -import { gql } from 'apollo-server-micro'; - -export default gql` - extend type Query { - unlockedCourses: [StorefrontCourse!]! - - unlockedCourse(courseId: CourseId!): UnlockedCourse - } - - extend type Mutation { - createFreeCourse( - courseId: CourseId! - bundleId: BundleId! - ): Boolean! - - createAdminCourse( - uid: String! - courseId: CourseId! - bundleId: BundleId! - ): Boolean! - } - - type UnlockedCourse { - courseId: CourseId! - bundleId: BundleId! - header: String! - url: String! - imageUrl: String! - canUpgrade: Boolean! - introduction: Introduction - onboarding: Onboarding - bookDownload: BookDownload - bookOnline: BookOnline - curriculum: Curriculum - } - - type Introduction { - label: String! - data: IntroductionData - } - - type IntroductionData { - label: String! - url: String! - description: String! - } - - type Onboarding { - label: String! - data: OnboardingData - } - - type OnboardingData { - items: [OnboardingItem!]! - } - - type OnboardingItem { - label: String! - url: String! - description: String! - secondaryUrl: String - } - - type BookDownload { - label: String! - data: BookDownloadData - } - - type BookDownloadData { - label: String! - items: [BookDownloadItem!]! - } - - type BookDownloadItem { - label: String! - description: String! - url: String! - fileName: String! - } - - type BookOnline { - label: String! - data: BookOnlineData - } - - type BookOnlineData { - chapters: [BookChapter!]! - } - - type BookChapter { - label: String! - url: String - sections: [BookSection!] - } - - type BookSection { - label: String! - url: String! - } - - type Curriculum { - label: String! - data: CurriculumData - } - - type CurriculumData { - sections: [CurriculumSection!]! - } - - type CurriculumSection { - label: String! - items: [CurriculumItem!]! - } - - type CurriculumItem { - label: String! - url: String! - description: String! - kind: Kind! - secondaryUrl: String - } - - enum Kind { - Article - Video - } -`; diff --git a/src/api/typeDefs/migration/index.ts b/src/api/typeDefs/migration/index.ts deleted file mode 100644 index e988687e..00000000 --- a/src/api/typeDefs/migration/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { gql } from 'apollo-server-micro'; - -export default gql` - extend type Mutation { - migrate(migrationType: String!): Boolean - } -`; diff --git a/src/api/typeDefs/partner/index.ts b/src/api/typeDefs/partner/index.ts deleted file mode 100644 index 00a42401..00000000 --- a/src/api/typeDefs/partner/index.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { gql } from 'apollo-server-micro'; - -export default gql` - extend type Query { - partnerVisitors(from: DateTime!, to: DateTime!): [VisitorByDay!]! - partnerSales(offset: Int!, limit: Int!): PartnerSaleConnection! - partnerPayments: [PartnerPayment!]! - } - - extend type Mutation { - promoteToPartner(uid: String!): Boolean - partnerTrackVisitor(partnerId: String!): Boolean - } - - type VisitorByDay { - date: DateTime! - count: Int! - } - - type PartnerSaleConnection { - edges: [PartnerSale!]! - pageInfo: PageInfo! - } - - type PartnerSale { - id: String! - createdAt: DateTime! - royalty: Int! - price: Int! - courseId: CourseId! - bundleId: BundleId! - isCoupon: Boolean! - } - - type PartnerPayment { - createdAt: DateTime! - royalty: Int! - } - - type PageInfo { - total: Int! - } -`; diff --git a/src/api/typeDefs/paypal/index.ts b/src/api/typeDefs/paypal/index.ts deleted file mode 100644 index 62bdc866..00000000 --- a/src/api/typeDefs/paypal/index.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { gql } from 'apollo-server-micro'; - -export default gql` - extend type Mutation { - paypalCreateOrder( - courseId: CourseId! - bundleId: BundleId! - coupon: String - partnerId: String - ): OrderId! - - paypalApproveOrder(orderId: String!): Boolean - } - - type OrderId { - orderId: String! - } -`; diff --git a/src/api/typeDefs/session/index.ts b/src/api/typeDefs/session/index.ts deleted file mode 100644 index 0a939e37..00000000 --- a/src/api/typeDefs/session/index.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { gql } from 'apollo-server-micro'; - -export default gql` - extend type Mutation { - signIn(email: String!, password: String!): SessionToken! - - signUp( - username: String! - email: String! - password: String! - ): SessionToken! - - passwordForgot(email: String!): Boolean - - passwordChange(password: String!): Boolean - - emailChange(email: String!): Boolean - } - - type SessionToken { - sessionToken: String! - } -`; diff --git a/src/api/typeDefs/storefront/index.ts b/src/api/typeDefs/storefront/index.ts deleted file mode 100644 index e2d20148..00000000 --- a/src/api/typeDefs/storefront/index.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { gql } from 'apollo-server-micro'; - -export default gql` - extend type Query { - storefrontCourse( - courseId: CourseId! - bundleId: BundleId! - ): StorefrontCourse - - storefrontCourses: [StorefrontCourse!]! - - storefrontBundles(courseId: CourseId!): [StorefrontBundle!]! - } - - type StorefrontCourse { - header: String! - courseId: CourseId! - url: String! - imageUrl: String! - canUpgrade: Boolean! - bundle: StorefrontBundle! - } - - type StorefrontBundle { - header: String! - bundleId: BundleId! - price: Int! - imageUrl: String! - benefits: [String!]! - } -`; diff --git a/src/api/typeDefs/stripe/index.ts b/src/api/typeDefs/stripe/index.ts deleted file mode 100644 index 4ea99c1d..00000000 --- a/src/api/typeDefs/stripe/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { gql } from 'apollo-server-micro'; - -export default gql` - extend type Mutation { - stripeCreateOrder( - imageUrl: String! - courseId: CourseId! - bundleId: BundleId! - coupon: String - partnerId: String - ): StripeId! - } - - type StripeId { - id: String! - } -`; diff --git a/src/api/typeDefs/upgrade/index.ts b/src/api/typeDefs/upgrade/index.ts deleted file mode 100644 index e45d4102..00000000 --- a/src/api/typeDefs/upgrade/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { gql } from 'apollo-server-micro'; - -export default gql` - extend type Query { - upgradeableCourses(courseId: CourseId!): [StorefrontCourse!]! - } -`; diff --git a/src/api/typeDefs/user/index.ts b/src/api/typeDefs/user/index.ts deleted file mode 100644 index 976a29a2..00000000 --- a/src/api/typeDefs/user/index.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { gql } from 'apollo-server-micro'; - -export default gql` - extend type Query { - me: User - } - - type User { - email: String! - uid: String! - username: String! - roles: [String!]! - } -`; diff --git a/src/connectors/course.ts b/src/connectors/course.ts index e704b7c5..78fa3976 100644 --- a/src/connectors/course.ts +++ b/src/connectors/course.ts @@ -1,7 +1,7 @@ import { Connection, Repository } from 'typeorm'; -import { COURSE } from '@data/course-keys'; -import { BUNDLE } from '@data/bundle-keys'; +import { COURSE } from '@data/course-keys-types'; +import { BUNDLE } from '@data/bundle-keys-types'; import { Course } from '@models/course'; export class CourseConnector { diff --git a/src/connectors/partner.ts b/src/connectors/partner.ts index 5b82734b..a8223209 100644 --- a/src/connectors/partner.ts +++ b/src/connectors/partner.ts @@ -1,7 +1,7 @@ import { Between } from 'typeorm'; import { Connection, Repository } from 'typeorm'; -import { VisitorByDay, PartnerPayment } from '@generated/client'; +import { VisitorByDay, PartnerPayment } from '@api/resolvers/partner'; import { PartnerVisitor, PartnerSale } from '@models/partner'; import { Course } from '@models/course'; import { PARTNER_PERCENTAGE } from '@constants/partner'; diff --git a/src/generated/client.tsx b/src/generated/client.tsx index 281f519c..4ac48625 100644 --- a/src/generated/client.tsx +++ b/src/generated/client.tsx @@ -58,19 +58,6 @@ export type BookSection = { url: Scalars['String']; }; -export enum BundleId { - Student = 'STUDENT', - Intermediate = 'INTERMEDIATE', - Professional = 'PROFESSIONAL' -} - -export enum CourseId { - TheRoadToLearnReact = 'THE_ROAD_TO_LEARN_REACT', - TamingTheState = 'TAMING_THE_STATE', - TheRoadToGraphql = 'THE_ROAD_TO_GRAPHQL', - TheRoadToReactWithFirebase = 'THE_ROAD_TO_REACT_WITH_FIREBASE' -} - export type Curriculum = { __typename?: 'Curriculum'; label: Scalars['String']; @@ -87,7 +74,7 @@ export type CurriculumItem = { label: Scalars['String']; url: Scalars['String']; description: Scalars['String']; - kind: Kind; + kind: Scalars['String']; secondaryUrl?: Maybe; }; @@ -100,7 +87,7 @@ export type CurriculumSection = { export type Discount = { __typename?: 'Discount'; - price: Scalars['Int']; + price: Scalars['Float']; isDiscount: Scalars['Boolean']; }; @@ -124,11 +111,6 @@ export type IntroductionData = { description: Scalars['String']; }; -export enum Kind { - Article = 'Article', - Video = 'Video' -} - export type Markdown = { __typename?: 'Markdown'; body: Scalars['String']; @@ -136,22 +118,21 @@ export type Markdown = { export type Mutation = { __typename?: 'Mutation'; - _?: Maybe; - migrate?: Maybe; + migrate: Scalars['Boolean']; signIn: SessionToken; signUp: SessionToken; passwordForgot?: Maybe; passwordChange?: Maybe; emailChange?: Maybe; - paypalCreateOrder: OrderId; - paypalApproveOrder?: Maybe; + paypalCreateOrder: PaypalOrderId; + paypalApproveOrder: Scalars['Boolean']; stripeCreateOrder: StripeId; createFreeCourse: Scalars['Boolean']; createAdminCourse: Scalars['Boolean']; couponCreate?: Maybe; - promoteToPartner?: Maybe; - partnerTrackVisitor?: Maybe; - communityJoin?: Maybe; + promoteToPartner: Scalars['Boolean']; + partnerTrackVisitor: Scalars['Boolean']; + communityJoin: Scalars['Boolean']; }; @@ -161,15 +142,15 @@ export type MutationMigrateArgs = { export type MutationSignInArgs = { - email: Scalars['String']; password: Scalars['String']; + email: Scalars['String']; }; export type MutationSignUpArgs = { - username: Scalars['String']; - email: Scalars['String']; password: Scalars['String']; + email: Scalars['String']; + username: Scalars['String']; }; @@ -189,10 +170,10 @@ export type MutationEmailChangeArgs = { export type MutationPaypalCreateOrderArgs = { - courseId: CourseId; - bundleId: BundleId; - coupon?: Maybe; partnerId?: Maybe; + coupon?: Maybe; + bundleId: Scalars['String']; + courseId: Scalars['String']; }; @@ -202,31 +183,31 @@ export type MutationPaypalApproveOrderArgs = { export type MutationStripeCreateOrderArgs = { - imageUrl: Scalars['String']; - courseId: CourseId; - bundleId: BundleId; - coupon?: Maybe; partnerId?: Maybe; + coupon?: Maybe; + bundleId: Scalars['String']; + courseId: Scalars['String']; + imageUrl: Scalars['String']; }; export type MutationCreateFreeCourseArgs = { - courseId: CourseId; - bundleId: BundleId; + bundleId: Scalars['String']; + courseId: Scalars['String']; }; export type MutationCreateAdminCourseArgs = { uid: Scalars['String']; - courseId: CourseId; - bundleId: BundleId; + bundleId: Scalars['String']; + courseId: Scalars['String']; }; export type MutationCouponCreateArgs = { + count: Scalars['Float']; + discount: Scalars['Float']; coupon: Scalars['String']; - discount: Scalars['Int']; - count: Scalars['Int']; }; @@ -263,30 +244,25 @@ export type OnboardingItem = { secondaryUrl?: Maybe; }; -export type OrderId = { - __typename?: 'OrderId'; - orderId: Scalars['String']; -}; - export type PageInfo = { __typename?: 'PageInfo'; - total: Scalars['Int']; + total: Scalars['Float']; }; export type PartnerPayment = { __typename?: 'PartnerPayment'; createdAt: Scalars['DateTime']; - royalty: Scalars['Int']; + royalty: Scalars['Float']; }; export type PartnerSale = { __typename?: 'PartnerSale'; id: Scalars['String']; createdAt: Scalars['DateTime']; - royalty: Scalars['Int']; - price: Scalars['Int']; - courseId: CourseId; - bundleId: BundleId; + royalty: Scalars['Float']; + price: Scalars['Float']; + courseId: Scalars['String']; + bundleId: Scalars['String']; isCoupon: Scalars['Boolean']; }; @@ -296,11 +272,15 @@ export type PartnerSaleConnection = { pageInfo: PageInfo; }; +export type PaypalOrderId = { + __typename?: 'PaypalOrderId'; + orderId?: Maybe; +}; + export type Query = { __typename?: 'Query'; - _?: Maybe; - me?: Maybe; - storefrontCourse?: Maybe; + me: User; + storefrontCourse: StorefrontCourse; storefrontCourses: Array; storefrontBundles: Array; unlockedCourses: Array; @@ -316,24 +296,24 @@ export type Query = { export type QueryStorefrontCourseArgs = { - courseId: CourseId; - bundleId: BundleId; + bundleId: Scalars['String']; + courseId: Scalars['String']; }; export type QueryStorefrontBundlesArgs = { - courseId: CourseId; + courseId: Scalars['String']; }; export type QueryUnlockedCourseArgs = { - courseId: CourseId; + courseId: Scalars['String']; }; export type QueryBookArgs = { - path: Scalars['String']; fileName: Scalars['String']; + path: Scalars['String']; }; @@ -343,26 +323,26 @@ export type QueryOnlineChapterArgs = { export type QueryUpgradeableCoursesArgs = { - courseId: CourseId; + courseId: Scalars['String']; }; export type QueryDiscountedPriceArgs = { - courseId: CourseId; - bundleId: BundleId; coupon: Scalars['String']; + bundleId: Scalars['String']; + courseId: Scalars['String']; }; export type QueryPartnerVisitorsArgs = { - from: Scalars['DateTime']; to: Scalars['DateTime']; + from: Scalars['DateTime']; }; export type QueryPartnerSalesArgs = { - offset: Scalars['Int']; - limit: Scalars['Int']; + limit: Scalars['Float']; + offset: Scalars['Float']; }; export type SessionToken = { @@ -373,8 +353,8 @@ export type SessionToken = { export type StorefrontBundle = { __typename?: 'StorefrontBundle'; header: Scalars['String']; - bundleId: BundleId; - price: Scalars['Int']; + bundleId: Scalars['String']; + price: Scalars['Float']; imageUrl: Scalars['String']; benefits: Array; }; @@ -382,7 +362,7 @@ export type StorefrontBundle = { export type StorefrontCourse = { __typename?: 'StorefrontCourse'; header: Scalars['String']; - courseId: CourseId; + courseId: Scalars['String']; url: Scalars['String']; imageUrl: Scalars['String']; canUpgrade: Scalars['Boolean']; @@ -391,18 +371,13 @@ export type StorefrontCourse = { export type StripeId = { __typename?: 'StripeId'; - id: Scalars['String']; -}; - -export type Subscription = { - __typename?: 'Subscription'; - _?: Maybe; + id?: Maybe; }; export type UnlockedCourse = { __typename?: 'UnlockedCourse'; - courseId: CourseId; - bundleId: BundleId; + courseId: Scalars['String']; + bundleId: Scalars['String']; header: Scalars['String']; url: Scalars['String']; imageUrl: Scalars['String']; @@ -425,7 +400,7 @@ export type User = { export type VisitorByDay = { __typename?: 'VisitorByDay'; date: Scalars['DateTime']; - count: Scalars['Int']; + count: Scalars['Float']; }; export type GetBookQueryVariables = { @@ -466,8 +441,8 @@ export type CommunityJoinMutation = ( ); export type GetDiscountedPriceQueryVariables = { - courseId: CourseId; - bundleId: BundleId; + courseId: Scalars['String']; + bundleId: Scalars['String']; coupon: Scalars['String']; }; @@ -482,8 +457,8 @@ export type GetDiscountedPriceQuery = ( export type CouponCreateMutationVariables = { coupon: Scalars['String']; - discount: Scalars['Int']; - count: Scalars['Int']; + discount: Scalars['Float']; + count: Scalars['Float']; }; @@ -504,7 +479,7 @@ export type GetCoursesQuery = ( ); export type GetCourseQueryVariables = { - courseId: CourseId; + courseId: Scalars['String']; }; @@ -573,8 +548,8 @@ export type GetCourseQuery = ( ); export type CreateFreeCourseMutationVariables = { - courseId: CourseId; - bundleId: BundleId; + courseId: Scalars['String']; + bundleId: Scalars['String']; }; @@ -585,8 +560,8 @@ export type CreateFreeCourseMutation = ( export type CreateAdminCourseMutationVariables = { uid: Scalars['String']; - courseId: CourseId; - bundleId: BundleId; + courseId: Scalars['String']; + bundleId: Scalars['String']; }; @@ -640,8 +615,8 @@ export type PartnerVisitorsQuery = ( ); export type PartnerSalesQueryVariables = { - offset: Scalars['Int']; - limit: Scalars['Int']; + offset: Scalars['Float']; + limit: Scalars['Float']; }; @@ -671,8 +646,8 @@ export type PartnerPaymentsQuery = ( ); export type PaypalCreateOrderMutationVariables = { - courseId: CourseId; - bundleId: BundleId; + courseId: Scalars['String']; + bundleId: Scalars['String']; coupon?: Maybe; partnerId?: Maybe; }; @@ -681,8 +656,8 @@ export type PaypalCreateOrderMutationVariables = { export type PaypalCreateOrderMutation = ( { __typename?: 'Mutation' } & { paypalCreateOrder: ( - { __typename?: 'OrderId' } - & Pick + { __typename?: 'PaypalOrderId' } + & Pick ) } ); @@ -756,21 +731,21 @@ export type EmailChangeMutation = ( ); export type GetStorefrontCourseQueryVariables = { - courseId: CourseId; - bundleId: BundleId; + courseId: Scalars['String']; + bundleId: Scalars['String']; }; export type GetStorefrontCourseQuery = ( { __typename?: 'Query' } - & { storefrontCourse?: Maybe<( + & { storefrontCourse: ( { __typename?: 'StorefrontCourse' } & Pick & { bundle: ( { __typename?: 'StorefrontBundle' } & Pick ) } - )> } + ) } ); export type GetStorefrontCoursesQueryVariables = {}; @@ -786,8 +761,8 @@ export type GetStorefrontCoursesQuery = ( export type StripeCreateOrderMutationVariables = { imageUrl: Scalars['String']; - courseId: CourseId; - bundleId: BundleId; + courseId: Scalars['String']; + bundleId: Scalars['String']; coupon?: Maybe; partnerId?: Maybe; }; @@ -802,7 +777,7 @@ export type StripeCreateOrderMutation = ( ); export type GetUpgradeableCoursesQueryVariables = { - courseId: CourseId; + courseId: Scalars['String']; }; @@ -823,10 +798,10 @@ export type GetMeQueryVariables = {}; export type GetMeQuery = ( { __typename?: 'Query' } - & { me?: Maybe<( + & { me: ( { __typename?: 'User' } & Pick - )> } + ) } ); @@ -930,7 +905,7 @@ export type CommunityJoinMutationHookResult = ReturnType; export type CommunityJoinMutationOptions = ApolloReactCommon.BaseMutationOptions; export const GetDiscountedPriceDocument = gql` - query GetDiscountedPrice($courseId: CourseId!, $bundleId: BundleId!, $coupon: String!) { + query GetDiscountedPrice($courseId: String!, $bundleId: String!, $coupon: String!) { discountedPrice(courseId: $courseId, bundleId: $bundleId, coupon: $coupon) { price isDiscount @@ -966,7 +941,7 @@ export type GetDiscountedPriceQueryHookResult = ReturnType; export type GetDiscountedPriceQueryResult = ApolloReactCommon.QueryResult; export const CouponCreateDocument = gql` - mutation CouponCreate($coupon: String!, $discount: Int!, $count: Int!) { + mutation CouponCreate($coupon: String!, $discount: Float!, $count: Float!) { couponCreate(coupon: $coupon, discount: $discount, count: $count) } `; @@ -1034,7 +1009,7 @@ export type GetCoursesQueryHookResult = ReturnType; export type GetCoursesLazyQueryHookResult = ReturnType; export type GetCoursesQueryResult = ApolloReactCommon.QueryResult; export const GetCourseDocument = gql` - query GetCourse($courseId: CourseId!) { + query GetCourse($courseId: String!) { unlockedCourse(courseId: $courseId) { courseId header @@ -1127,7 +1102,7 @@ export type GetCourseQueryHookResult = ReturnType; export type GetCourseLazyQueryHookResult = ReturnType; export type GetCourseQueryResult = ApolloReactCommon.QueryResult; export const CreateFreeCourseDocument = gql` - mutation CreateFreeCourse($courseId: CourseId!, $bundleId: BundleId!) { + mutation CreateFreeCourse($courseId: String!, $bundleId: String!) { createFreeCourse(courseId: $courseId, bundleId: $bundleId) } `; @@ -1158,7 +1133,7 @@ export type CreateFreeCourseMutationHookResult = ReturnType; export type CreateFreeCourseMutationOptions = ApolloReactCommon.BaseMutationOptions; export const CreateAdminCourseDocument = gql` - mutation CreateAdminCourse($uid: String!, $courseId: CourseId!, $bundleId: BundleId!) { + mutation CreateAdminCourse($uid: String!, $courseId: String!, $bundleId: String!) { createAdminCourse(uid: $uid, courseId: $courseId, bundleId: $bundleId) } `; @@ -1315,7 +1290,7 @@ export type PartnerVisitorsQueryHookResult = ReturnType; export type PartnerVisitorsQueryResult = ApolloReactCommon.QueryResult; export const PartnerSalesDocument = gql` - query PartnerSales($offset: Int!, $limit: Int!) { + query PartnerSales($offset: Float!, $limit: Float!) { partnerSales(offset: $offset, limit: $limit) { edges { id @@ -1393,7 +1368,7 @@ export type PartnerPaymentsQueryHookResult = ReturnType; export type PartnerPaymentsQueryResult = ApolloReactCommon.QueryResult; export const PaypalCreateOrderDocument = gql` - mutation PaypalCreateOrder($courseId: CourseId!, $bundleId: BundleId!, $coupon: String, $partnerId: String) { + mutation PaypalCreateOrder($courseId: String!, $bundleId: String!, $coupon: String, $partnerId: String) { paypalCreateOrder(courseId: $courseId, bundleId: $bundleId, coupon: $coupon, partnerId: $partnerId) { orderId } @@ -1615,7 +1590,7 @@ export type EmailChangeMutationHookResult = ReturnType; export type EmailChangeMutationOptions = ApolloReactCommon.BaseMutationOptions; export const GetStorefrontCourseDocument = gql` - query GetStorefrontCourse($courseId: CourseId!, $bundleId: BundleId!) { + query GetStorefrontCourse($courseId: String!, $bundleId: String!) { storefrontCourse(courseId: $courseId, bundleId: $bundleId) { header courseId @@ -1691,7 +1666,7 @@ export type GetStorefrontCoursesQueryHookResult = ReturnType; export type GetStorefrontCoursesQueryResult = ApolloReactCommon.QueryResult; export const StripeCreateOrderDocument = gql` - mutation StripeCreateOrder($imageUrl: String!, $courseId: CourseId!, $bundleId: BundleId!, $coupon: String, $partnerId: String) { + mutation StripeCreateOrder($imageUrl: String!, $courseId: String!, $bundleId: String!, $coupon: String, $partnerId: String) { stripeCreateOrder(imageUrl: $imageUrl, courseId: $courseId, bundleId: $bundleId, coupon: $coupon, partnerId: $partnerId) { id } @@ -1727,7 +1702,7 @@ export type StripeCreateOrderMutationHookResult = ReturnType; export type StripeCreateOrderMutationOptions = ApolloReactCommon.BaseMutationOptions; export const GetUpgradeableCoursesDocument = gql` - query GetUpgradeableCourses($courseId: CourseId!) { + query GetUpgradeableCourses($courseId: String!) { upgradeableCourses(courseId: $courseId) { header courseId diff --git a/src/generated/server.ts b/src/generated/server.ts deleted file mode 100644 index 5a947462..00000000 --- a/src/generated/server.ts +++ /dev/null @@ -1,889 +0,0 @@ -import { GraphQLResolveInfo, GraphQLScalarType, GraphQLScalarTypeConfig } from 'graphql'; -import { ResolverContext } from '@typeDefs/resolver'; -export type Maybe = T | null; -export type RequireFields = { [X in Exclude]?: T[X] } & { [P in K]-?: NonNullable }; -/** All built-in and custom scalars, mapped to their actual values */ -export type Scalars = { - ID: string; - String: string; - Boolean: boolean; - Int: number; - Float: number; - DateTime: any; -}; - -export type BookChapter = { - __typename?: 'BookChapter'; - label: Scalars['String']; - url?: Maybe; - sections?: Maybe>; -}; - -export type BookDownload = { - __typename?: 'BookDownload'; - label: Scalars['String']; - data?: Maybe; -}; - -export type BookDownloadData = { - __typename?: 'BookDownloadData'; - label: Scalars['String']; - items: Array; -}; - -export type BookDownloadItem = { - __typename?: 'BookDownloadItem'; - label: Scalars['String']; - description: Scalars['String']; - url: Scalars['String']; - fileName: Scalars['String']; -}; - -export type BookOnline = { - __typename?: 'BookOnline'; - label: Scalars['String']; - data?: Maybe; -}; - -export type BookOnlineData = { - __typename?: 'BookOnlineData'; - chapters: Array; -}; - -export type BookSection = { - __typename?: 'BookSection'; - label: Scalars['String']; - url: Scalars['String']; -}; - -export enum BundleId { - Student = 'STUDENT', - Intermediate = 'INTERMEDIATE', - Professional = 'PROFESSIONAL' -} - -export enum CourseId { - TheRoadToLearnReact = 'THE_ROAD_TO_LEARN_REACT', - TamingTheState = 'TAMING_THE_STATE', - TheRoadToGraphql = 'THE_ROAD_TO_GRAPHQL', - TheRoadToReactWithFirebase = 'THE_ROAD_TO_REACT_WITH_FIREBASE' -} - -export type Curriculum = { - __typename?: 'Curriculum'; - label: Scalars['String']; - data?: Maybe; -}; - -export type CurriculumData = { - __typename?: 'CurriculumData'; - sections: Array; -}; - -export type CurriculumItem = { - __typename?: 'CurriculumItem'; - label: Scalars['String']; - url: Scalars['String']; - description: Scalars['String']; - kind: Kind; - secondaryUrl?: Maybe; -}; - -export type CurriculumSection = { - __typename?: 'CurriculumSection'; - label: Scalars['String']; - items: Array; -}; - - -export type Discount = { - __typename?: 'Discount'; - price: Scalars['Int']; - isDiscount: Scalars['Boolean']; -}; - -export type File = { - __typename?: 'File'; - fileName: Scalars['String']; - contentType: Scalars['String']; - body: Scalars['String']; -}; - -export type Introduction = { - __typename?: 'Introduction'; - label: Scalars['String']; - data?: Maybe; -}; - -export type IntroductionData = { - __typename?: 'IntroductionData'; - label: Scalars['String']; - url: Scalars['String']; - description: Scalars['String']; -}; - -export enum Kind { - Article = 'Article', - Video = 'Video' -} - -export type Markdown = { - __typename?: 'Markdown'; - body: Scalars['String']; -}; - -export type Mutation = { - __typename?: 'Mutation'; - _?: Maybe; - migrate?: Maybe; - signIn: SessionToken; - signUp: SessionToken; - passwordForgot?: Maybe; - passwordChange?: Maybe; - emailChange?: Maybe; - paypalCreateOrder: OrderId; - paypalApproveOrder?: Maybe; - stripeCreateOrder: StripeId; - createFreeCourse: Scalars['Boolean']; - createAdminCourse: Scalars['Boolean']; - couponCreate?: Maybe; - promoteToPartner?: Maybe; - partnerTrackVisitor?: Maybe; - communityJoin?: Maybe; -}; - - -export type MutationMigrateArgs = { - migrationType: Scalars['String']; -}; - - -export type MutationSignInArgs = { - email: Scalars['String']; - password: Scalars['String']; -}; - - -export type MutationSignUpArgs = { - username: Scalars['String']; - email: Scalars['String']; - password: Scalars['String']; -}; - - -export type MutationPasswordForgotArgs = { - email: Scalars['String']; -}; - - -export type MutationPasswordChangeArgs = { - password: Scalars['String']; -}; - - -export type MutationEmailChangeArgs = { - email: Scalars['String']; -}; - - -export type MutationPaypalCreateOrderArgs = { - courseId: CourseId; - bundleId: BundleId; - coupon?: Maybe; - partnerId?: Maybe; -}; - - -export type MutationPaypalApproveOrderArgs = { - orderId: Scalars['String']; -}; - - -export type MutationStripeCreateOrderArgs = { - imageUrl: Scalars['String']; - courseId: CourseId; - bundleId: BundleId; - coupon?: Maybe; - partnerId?: Maybe; -}; - - -export type MutationCreateFreeCourseArgs = { - courseId: CourseId; - bundleId: BundleId; -}; - - -export type MutationCreateAdminCourseArgs = { - uid: Scalars['String']; - courseId: CourseId; - bundleId: BundleId; -}; - - -export type MutationCouponCreateArgs = { - coupon: Scalars['String']; - discount: Scalars['Int']; - count: Scalars['Int']; -}; - - -export type MutationPromoteToPartnerArgs = { - uid: Scalars['String']; -}; - - -export type MutationPartnerTrackVisitorArgs = { - partnerId: Scalars['String']; -}; - - -export type MutationCommunityJoinArgs = { - email: Scalars['String']; -}; - -export type Onboarding = { - __typename?: 'Onboarding'; - label: Scalars['String']; - data?: Maybe; -}; - -export type OnboardingData = { - __typename?: 'OnboardingData'; - items: Array; -}; - -export type OnboardingItem = { - __typename?: 'OnboardingItem'; - label: Scalars['String']; - url: Scalars['String']; - description: Scalars['String']; - secondaryUrl?: Maybe; -}; - -export type OrderId = { - __typename?: 'OrderId'; - orderId: Scalars['String']; -}; - -export type PageInfo = { - __typename?: 'PageInfo'; - total: Scalars['Int']; -}; - -export type PartnerPayment = { - __typename?: 'PartnerPayment'; - createdAt: Scalars['DateTime']; - royalty: Scalars['Int']; -}; - -export type PartnerSale = { - __typename?: 'PartnerSale'; - id: Scalars['String']; - createdAt: Scalars['DateTime']; - royalty: Scalars['Int']; - price: Scalars['Int']; - courseId: CourseId; - bundleId: BundleId; - isCoupon: Scalars['Boolean']; -}; - -export type PartnerSaleConnection = { - __typename?: 'PartnerSaleConnection'; - edges: Array; - pageInfo: PageInfo; -}; - -export type Query = { - __typename?: 'Query'; - _?: Maybe; - me?: Maybe; - storefrontCourse?: Maybe; - storefrontCourses: Array; - storefrontBundles: Array; - unlockedCourses: Array; - unlockedCourse?: Maybe; - book: File; - onlineChapter: Markdown; - upgradeableCourses: Array; - discountedPrice: Discount; - partnerVisitors: Array; - partnerSales: PartnerSaleConnection; - partnerPayments: Array; -}; - - -export type QueryStorefrontCourseArgs = { - courseId: CourseId; - bundleId: BundleId; -}; - - -export type QueryStorefrontBundlesArgs = { - courseId: CourseId; -}; - - -export type QueryUnlockedCourseArgs = { - courseId: CourseId; -}; - - -export type QueryBookArgs = { - path: Scalars['String']; - fileName: Scalars['String']; -}; - - -export type QueryOnlineChapterArgs = { - path: Scalars['String']; -}; - - -export type QueryUpgradeableCoursesArgs = { - courseId: CourseId; -}; - - -export type QueryDiscountedPriceArgs = { - courseId: CourseId; - bundleId: BundleId; - coupon: Scalars['String']; -}; - - -export type QueryPartnerVisitorsArgs = { - from: Scalars['DateTime']; - to: Scalars['DateTime']; -}; - - -export type QueryPartnerSalesArgs = { - offset: Scalars['Int']; - limit: Scalars['Int']; -}; - -export type SessionToken = { - __typename?: 'SessionToken'; - sessionToken: Scalars['String']; -}; - -export type StorefrontBundle = { - __typename?: 'StorefrontBundle'; - header: Scalars['String']; - bundleId: BundleId; - price: Scalars['Int']; - imageUrl: Scalars['String']; - benefits: Array; -}; - -export type StorefrontCourse = { - __typename?: 'StorefrontCourse'; - header: Scalars['String']; - courseId: CourseId; - url: Scalars['String']; - imageUrl: Scalars['String']; - canUpgrade: Scalars['Boolean']; - bundle: StorefrontBundle; -}; - -export type StripeId = { - __typename?: 'StripeId'; - id: Scalars['String']; -}; - -export type Subscription = { - __typename?: 'Subscription'; - _?: Maybe; -}; - -export type UnlockedCourse = { - __typename?: 'UnlockedCourse'; - courseId: CourseId; - bundleId: BundleId; - header: Scalars['String']; - url: Scalars['String']; - imageUrl: Scalars['String']; - canUpgrade: Scalars['Boolean']; - introduction?: Maybe; - onboarding?: Maybe; - bookDownload?: Maybe; - bookOnline?: Maybe; - curriculum?: Maybe; -}; - -export type User = { - __typename?: 'User'; - email: Scalars['String']; - uid: Scalars['String']; - username: Scalars['String']; - roles: Array; -}; - -export type VisitorByDay = { - __typename?: 'VisitorByDay'; - date: Scalars['DateTime']; - count: Scalars['Int']; -}; - -export type WithIndex = TObject & Record; -export type ResolversObject = WithIndex; - -export type ResolverTypeWrapper = Promise | T; - - -export type StitchingResolver = { - fragment: string; - resolve: ResolverFn; -}; - -export type Resolver = - | ResolverFn - | StitchingResolver; - -export type ResolverFn = ( - parent: TParent, - args: TArgs, - context: TContext, - info: GraphQLResolveInfo -) => Promise | TResult; - -export type SubscriptionSubscribeFn = ( - parent: TParent, - args: TArgs, - context: TContext, - info: GraphQLResolveInfo -) => AsyncIterator | Promise>; - -export type SubscriptionResolveFn = ( - parent: TParent, - args: TArgs, - context: TContext, - info: GraphQLResolveInfo -) => TResult | Promise; - -export interface SubscriptionSubscriberObject { - subscribe: SubscriptionSubscribeFn<{ [key in TKey]: TResult }, TParent, TContext, TArgs>; - resolve?: SubscriptionResolveFn; -} - -export interface SubscriptionResolverObject { - subscribe: SubscriptionSubscribeFn; - resolve: SubscriptionResolveFn; -} - -export type SubscriptionObject = - | SubscriptionSubscriberObject - | SubscriptionResolverObject; - -export type SubscriptionResolver = - | ((...args: any[]) => SubscriptionObject) - | SubscriptionObject; - -export type TypeResolveFn = ( - parent: TParent, - context: TContext, - info: GraphQLResolveInfo -) => Maybe | Promise>; - -export type isTypeOfResolverFn = (obj: T, info: GraphQLResolveInfo) => boolean | Promise; - -export type NextResolverFn = () => Promise; - -export type DirectiveResolverFn = ( - next: NextResolverFn, - parent: TParent, - args: TArgs, - context: TContext, - info: GraphQLResolveInfo -) => TResult | Promise; - -/** Mapping between all available schema types and the resolvers types */ -export type ResolversTypes = ResolversObject<{ - Query: ResolverTypeWrapper<{}>, - Boolean: ResolverTypeWrapper, - User: ResolverTypeWrapper, - String: ResolverTypeWrapper, - CourseId: ResolverTypeWrapper, - BundleId: ResolverTypeWrapper, - StorefrontCourse: ResolverTypeWrapper, - StorefrontBundle: ResolverTypeWrapper, - Int: ResolverTypeWrapper, - UnlockedCourse: ResolverTypeWrapper, - Introduction: ResolverTypeWrapper, - IntroductionData: ResolverTypeWrapper, - Onboarding: ResolverTypeWrapper, - OnboardingData: ResolverTypeWrapper, - OnboardingItem: ResolverTypeWrapper, - BookDownload: ResolverTypeWrapper, - BookDownloadData: ResolverTypeWrapper, - BookDownloadItem: ResolverTypeWrapper, - BookOnline: ResolverTypeWrapper, - BookOnlineData: ResolverTypeWrapper, - BookChapter: ResolverTypeWrapper, - BookSection: ResolverTypeWrapper, - Curriculum: ResolverTypeWrapper, - CurriculumData: ResolverTypeWrapper, - CurriculumSection: ResolverTypeWrapper, - CurriculumItem: ResolverTypeWrapper, - Kind: ResolverTypeWrapper, - File: ResolverTypeWrapper, - Markdown: ResolverTypeWrapper, - Discount: ResolverTypeWrapper, - DateTime: ResolverTypeWrapper, - VisitorByDay: ResolverTypeWrapper, - PartnerSaleConnection: ResolverTypeWrapper, - PartnerSale: ResolverTypeWrapper, - PageInfo: ResolverTypeWrapper, - PartnerPayment: ResolverTypeWrapper, - Mutation: ResolverTypeWrapper<{}>, - SessionToken: ResolverTypeWrapper, - OrderId: ResolverTypeWrapper, - StripeId: ResolverTypeWrapper, - Subscription: ResolverTypeWrapper<{}>, -}>; - -/** Mapping between all available schema types and the resolvers parents */ -export type ResolversParentTypes = ResolversObject<{ - Query: {}, - Boolean: any, - User: any, - String: any, - CourseId: any, - BundleId: any, - StorefrontCourse: any, - StorefrontBundle: any, - Int: any, - UnlockedCourse: any, - Introduction: any, - IntroductionData: any, - Onboarding: any, - OnboardingData: any, - OnboardingItem: any, - BookDownload: any, - BookDownloadData: any, - BookDownloadItem: any, - BookOnline: any, - BookOnlineData: any, - BookChapter: any, - BookSection: any, - Curriculum: any, - CurriculumData: any, - CurriculumSection: any, - CurriculumItem: any, - Kind: any, - File: any, - Markdown: any, - Discount: any, - DateTime: any, - VisitorByDay: any, - PartnerSaleConnection: any, - PartnerSale: any, - PageInfo: any, - PartnerPayment: any, - Mutation: {}, - SessionToken: any, - OrderId: any, - StripeId: any, - Subscription: {}, -}>; - -export type BookChapterResolvers = ResolversObject<{ - label?: Resolver, - url?: Resolver, ParentType, ContextType>, - sections?: Resolver>, ParentType, ContextType>, - __isTypeOf?: isTypeOfResolverFn, -}>; - -export type BookDownloadResolvers = ResolversObject<{ - label?: Resolver, - data?: Resolver, ParentType, ContextType>, - __isTypeOf?: isTypeOfResolverFn, -}>; - -export type BookDownloadDataResolvers = ResolversObject<{ - label?: Resolver, - items?: Resolver, ParentType, ContextType>, - __isTypeOf?: isTypeOfResolverFn, -}>; - -export type BookDownloadItemResolvers = ResolversObject<{ - label?: Resolver, - description?: Resolver, - url?: Resolver, - fileName?: Resolver, - __isTypeOf?: isTypeOfResolverFn, -}>; - -export type BookOnlineResolvers = ResolversObject<{ - label?: Resolver, - data?: Resolver, ParentType, ContextType>, - __isTypeOf?: isTypeOfResolverFn, -}>; - -export type BookOnlineDataResolvers = ResolversObject<{ - chapters?: Resolver, ParentType, ContextType>, - __isTypeOf?: isTypeOfResolverFn, -}>; - -export type BookSectionResolvers = ResolversObject<{ - label?: Resolver, - url?: Resolver, - __isTypeOf?: isTypeOfResolverFn, -}>; - -export type CurriculumResolvers = ResolversObject<{ - label?: Resolver, - data?: Resolver, ParentType, ContextType>, - __isTypeOf?: isTypeOfResolverFn, -}>; - -export type CurriculumDataResolvers = ResolversObject<{ - sections?: Resolver, ParentType, ContextType>, - __isTypeOf?: isTypeOfResolverFn, -}>; - -export type CurriculumItemResolvers = ResolversObject<{ - label?: Resolver, - url?: Resolver, - description?: Resolver, - kind?: Resolver, - secondaryUrl?: Resolver, ParentType, ContextType>, - __isTypeOf?: isTypeOfResolverFn, -}>; - -export type CurriculumSectionResolvers = ResolversObject<{ - label?: Resolver, - items?: Resolver, ParentType, ContextType>, - __isTypeOf?: isTypeOfResolverFn, -}>; - -export interface DateTimeScalarConfig extends GraphQLScalarTypeConfig { - name: 'DateTime' -} - -export type DiscountResolvers = ResolversObject<{ - price?: Resolver, - isDiscount?: Resolver, - __isTypeOf?: isTypeOfResolverFn, -}>; - -export type FileResolvers = ResolversObject<{ - fileName?: Resolver, - contentType?: Resolver, - body?: Resolver, - __isTypeOf?: isTypeOfResolverFn, -}>; - -export type IntroductionResolvers = ResolversObject<{ - label?: Resolver, - data?: Resolver, ParentType, ContextType>, - __isTypeOf?: isTypeOfResolverFn, -}>; - -export type IntroductionDataResolvers = ResolversObject<{ - label?: Resolver, - url?: Resolver, - description?: Resolver, - __isTypeOf?: isTypeOfResolverFn, -}>; - -export type MarkdownResolvers = ResolversObject<{ - body?: Resolver, - __isTypeOf?: isTypeOfResolverFn, -}>; - -export type MutationResolvers = ResolversObject<{ - _?: Resolver, ParentType, ContextType>, - migrate?: Resolver, ParentType, ContextType, RequireFields>, - signIn?: Resolver>, - signUp?: Resolver>, - passwordForgot?: Resolver, ParentType, ContextType, RequireFields>, - passwordChange?: Resolver, ParentType, ContextType, RequireFields>, - emailChange?: Resolver, ParentType, ContextType, RequireFields>, - paypalCreateOrder?: Resolver>, - paypalApproveOrder?: Resolver, ParentType, ContextType, RequireFields>, - stripeCreateOrder?: Resolver>, - createFreeCourse?: Resolver>, - createAdminCourse?: Resolver>, - couponCreate?: Resolver, ParentType, ContextType, RequireFields>, - promoteToPartner?: Resolver, ParentType, ContextType, RequireFields>, - partnerTrackVisitor?: Resolver, ParentType, ContextType, RequireFields>, - communityJoin?: Resolver, ParentType, ContextType, RequireFields>, -}>; - -export type OnboardingResolvers = ResolversObject<{ - label?: Resolver, - data?: Resolver, ParentType, ContextType>, - __isTypeOf?: isTypeOfResolverFn, -}>; - -export type OnboardingDataResolvers = ResolversObject<{ - items?: Resolver, ParentType, ContextType>, - __isTypeOf?: isTypeOfResolverFn, -}>; - -export type OnboardingItemResolvers = ResolversObject<{ - label?: Resolver, - url?: Resolver, - description?: Resolver, - secondaryUrl?: Resolver, ParentType, ContextType>, - __isTypeOf?: isTypeOfResolverFn, -}>; - -export type OrderIdResolvers = ResolversObject<{ - orderId?: Resolver, - __isTypeOf?: isTypeOfResolverFn, -}>; - -export type PageInfoResolvers = ResolversObject<{ - total?: Resolver, - __isTypeOf?: isTypeOfResolverFn, -}>; - -export type PartnerPaymentResolvers = ResolversObject<{ - createdAt?: Resolver, - royalty?: Resolver, - __isTypeOf?: isTypeOfResolverFn, -}>; - -export type PartnerSaleResolvers = ResolversObject<{ - id?: Resolver, - createdAt?: Resolver, - royalty?: Resolver, - price?: Resolver, - courseId?: Resolver, - bundleId?: Resolver, - isCoupon?: Resolver, - __isTypeOf?: isTypeOfResolverFn, -}>; - -export type PartnerSaleConnectionResolvers = ResolversObject<{ - edges?: Resolver, ParentType, ContextType>, - pageInfo?: Resolver, - __isTypeOf?: isTypeOfResolverFn, -}>; - -export type QueryResolvers = ResolversObject<{ - _?: Resolver, ParentType, ContextType>, - me?: Resolver, ParentType, ContextType>, - storefrontCourse?: Resolver, ParentType, ContextType, RequireFields>, - storefrontCourses?: Resolver, ParentType, ContextType>, - storefrontBundles?: Resolver, ParentType, ContextType, RequireFields>, - unlockedCourses?: Resolver, ParentType, ContextType>, - unlockedCourse?: Resolver, ParentType, ContextType, RequireFields>, - book?: Resolver>, - onlineChapter?: Resolver>, - upgradeableCourses?: Resolver, ParentType, ContextType, RequireFields>, - discountedPrice?: Resolver>, - partnerVisitors?: Resolver, ParentType, ContextType, RequireFields>, - partnerSales?: Resolver>, - partnerPayments?: Resolver, ParentType, ContextType>, -}>; - -export type SessionTokenResolvers = ResolversObject<{ - sessionToken?: Resolver, - __isTypeOf?: isTypeOfResolverFn, -}>; - -export type StorefrontBundleResolvers = ResolversObject<{ - header?: Resolver, - bundleId?: Resolver, - price?: Resolver, - imageUrl?: Resolver, - benefits?: Resolver, ParentType, ContextType>, - __isTypeOf?: isTypeOfResolverFn, -}>; - -export type StorefrontCourseResolvers = ResolversObject<{ - header?: Resolver, - courseId?: Resolver, - url?: Resolver, - imageUrl?: Resolver, - canUpgrade?: Resolver, - bundle?: Resolver, - __isTypeOf?: isTypeOfResolverFn, -}>; - -export type StripeIdResolvers = ResolversObject<{ - id?: Resolver, - __isTypeOf?: isTypeOfResolverFn, -}>; - -export type SubscriptionResolvers = ResolversObject<{ - _?: SubscriptionResolver, "_", ParentType, ContextType>, -}>; - -export type UnlockedCourseResolvers = ResolversObject<{ - courseId?: Resolver, - bundleId?: Resolver, - header?: Resolver, - url?: Resolver, - imageUrl?: Resolver, - canUpgrade?: Resolver, - introduction?: Resolver, ParentType, ContextType>, - onboarding?: Resolver, ParentType, ContextType>, - bookDownload?: Resolver, ParentType, ContextType>, - bookOnline?: Resolver, ParentType, ContextType>, - curriculum?: Resolver, ParentType, ContextType>, - __isTypeOf?: isTypeOfResolverFn, -}>; - -export type UserResolvers = ResolversObject<{ - email?: Resolver, - uid?: Resolver, - username?: Resolver, - roles?: Resolver, ParentType, ContextType>, - __isTypeOf?: isTypeOfResolverFn, -}>; - -export type VisitorByDayResolvers = ResolversObject<{ - date?: Resolver, - count?: Resolver, - __isTypeOf?: isTypeOfResolverFn, -}>; - -export type Resolvers = ResolversObject<{ - BookChapter?: BookChapterResolvers, - BookDownload?: BookDownloadResolvers, - BookDownloadData?: BookDownloadDataResolvers, - BookDownloadItem?: BookDownloadItemResolvers, - BookOnline?: BookOnlineResolvers, - BookOnlineData?: BookOnlineDataResolvers, - BookSection?: BookSectionResolvers, - Curriculum?: CurriculumResolvers, - CurriculumData?: CurriculumDataResolvers, - CurriculumItem?: CurriculumItemResolvers, - CurriculumSection?: CurriculumSectionResolvers, - DateTime?: GraphQLScalarType, - Discount?: DiscountResolvers, - File?: FileResolvers, - Introduction?: IntroductionResolvers, - IntroductionData?: IntroductionDataResolvers, - Markdown?: MarkdownResolvers, - Mutation?: MutationResolvers, - Onboarding?: OnboardingResolvers, - OnboardingData?: OnboardingDataResolvers, - OnboardingItem?: OnboardingItemResolvers, - OrderId?: OrderIdResolvers, - PageInfo?: PageInfoResolvers, - PartnerPayment?: PartnerPaymentResolvers, - PartnerSale?: PartnerSaleResolvers, - PartnerSaleConnection?: PartnerSaleConnectionResolvers, - Query?: QueryResolvers, - SessionToken?: SessionTokenResolvers, - StorefrontBundle?: StorefrontBundleResolvers, - StorefrontCourse?: StorefrontCourseResolvers, - StripeId?: StripeIdResolvers, - Subscription?: SubscriptionResolvers, - UnlockedCourse?: UnlockedCourseResolvers, - User?: UserResolvers, - VisitorByDay?: VisitorByDayResolvers, -}>; - - -/** - * @deprecated - * Use "Resolvers" root object instead. If you wish to get "IResolvers", add "typesPrefix: I" to your config. -*/ -export type IResolvers = Resolvers; diff --git a/src/models/course.ts b/src/models/course.ts index eedc9d47..bbb91c6d 100644 --- a/src/models/course.ts +++ b/src/models/course.ts @@ -6,8 +6,8 @@ import { PrimaryGeneratedColumn, } from 'typeorm'; -import { COURSE } from '@data/course-keys'; -import { BUNDLE } from '@data/bundle-keys'; +import { COURSE } from '@data/course-keys-types'; +import { BUNDLE } from '@data/bundle-keys-types'; @Entity() export class Course { diff --git a/src/models/index.ts b/src/models/index.ts index 28ef7e20..2d763ad5 100644 --- a/src/models/index.ts +++ b/src/models/index.ts @@ -1,4 +1,3 @@ -import 'reflect-metadata'; import { createConnection, getConnection } from 'typeorm'; import * as CourseEntities from './course'; diff --git a/src/queries/coupon.ts b/src/queries/coupon.ts index 22a153df..e2f08917 100644 --- a/src/queries/coupon.ts +++ b/src/queries/coupon.ts @@ -2,8 +2,8 @@ import gql from 'graphql-tag'; export const GET_DISCOUNTED_PRICE = gql` query GetDiscountedPrice( - $courseId: CourseId! - $bundleId: BundleId! + $courseId: String! + $bundleId: String! $coupon: String! ) { discountedPrice( @@ -20,8 +20,8 @@ export const GET_DISCOUNTED_PRICE = gql` export const COUPON_CREATE = gql` mutation CouponCreate( $coupon: String! - $discount: Int! - $count: Int! + $discount: Float! + $count: Float! ) { couponCreate(coupon: $coupon, discount: $discount, count: $count) } diff --git a/src/queries/course.ts b/src/queries/course.ts index 365804a0..e8270023 100644 --- a/src/queries/course.ts +++ b/src/queries/course.ts @@ -13,7 +13,7 @@ export const GET_UNLOCKED_COURSES = gql` `; export const GET_UNLOCKED_COURSE = gql` - query GetCourse($courseId: CourseId!) { + query GetCourse($courseId: String!) { unlockedCourse(courseId: $courseId) { courseId header @@ -81,10 +81,7 @@ export const GET_UNLOCKED_COURSE = gql` `; export const CREATE_FREE_COURSE = gql` - mutation CreateFreeCourse( - $courseId: CourseId! - $bundleId: BundleId! - ) { + mutation CreateFreeCourse($courseId: String!, $bundleId: String!) { createFreeCourse(courseId: $courseId, bundleId: $bundleId) } `; @@ -92,8 +89,8 @@ export const CREATE_FREE_COURSE = gql` export const CREATE_ADMIN_COURSE = gql` mutation CreateAdminCourse( $uid: String! - $courseId: CourseId! - $bundleId: BundleId! + $courseId: String! + $bundleId: String! ) { createAdminCourse( uid: $uid diff --git a/src/queries/partner.ts b/src/queries/partner.ts index ad66f6dc..1f60fbcc 100644 --- a/src/queries/partner.ts +++ b/src/queries/partner.ts @@ -22,7 +22,7 @@ export const PARTNER_VISITORS = gql` `; export const PARTNER_SALES = gql` - query PartnerSales($offset: Int!, $limit: Int!) { + query PartnerSales($offset: Float!, $limit: Float!) { partnerSales(offset: $offset, limit: $limit) { edges { id diff --git a/src/queries/paypal.ts b/src/queries/paypal.ts index 6a2d7307..feab3fb1 100644 --- a/src/queries/paypal.ts +++ b/src/queries/paypal.ts @@ -2,8 +2,8 @@ import gql from 'graphql-tag'; export const PAYPAL_CREATE_ORDER = gql` mutation PaypalCreateOrder( - $courseId: CourseId! - $bundleId: BundleId! + $courseId: String! + $bundleId: String! $coupon: String $partnerId: String ) { diff --git a/src/queries/storefront.ts b/src/queries/storefront.ts index 681200dc..9c88bc39 100644 --- a/src/queries/storefront.ts +++ b/src/queries/storefront.ts @@ -1,10 +1,7 @@ import gql from 'graphql-tag'; export const GET_STOREFRONT_COURSE = gql` - query GetStorefrontCourse( - $courseId: CourseId! - $bundleId: BundleId! - ) { + query GetStorefrontCourse($courseId: String!, $bundleId: String!) { storefrontCourse(courseId: $courseId, bundleId: $bundleId) { header courseId diff --git a/src/queries/stripe.ts b/src/queries/stripe.ts index ffddd6ed..c4a9c123 100644 --- a/src/queries/stripe.ts +++ b/src/queries/stripe.ts @@ -3,8 +3,8 @@ import gql from 'graphql-tag'; export const STRIPE_CREATE_ORDER = gql` mutation StripeCreateOrder( $imageUrl: String! - $courseId: CourseId! - $bundleId: BundleId! + $courseId: String! + $bundleId: String! $coupon: String $partnerId: String ) { diff --git a/src/queries/upgrade.ts b/src/queries/upgrade.ts index dd4a4822..54fc4bd4 100644 --- a/src/queries/upgrade.ts +++ b/src/queries/upgrade.ts @@ -1,7 +1,7 @@ import gql from 'graphql-tag'; export const GET_UPGRADEABLE_COURSES = gql` - query GetUpgradeableCourses($courseId: CourseId!) { + query GetUpgradeableCourses($courseId: String!) { upgradeableCourses(courseId: $courseId) { header courseId diff --git a/src/services/course/index.ts b/src/services/course/index.ts index 1624e0c9..f8143802 100644 --- a/src/services/course/index.ts +++ b/src/services/course/index.ts @@ -1,7 +1,7 @@ import omit from 'lodash.omit'; import { Course } from '@models/course'; -import { COURSE } from '@data/course-keys'; +import { COURSE } from '@data/course-keys-types'; import BUNDLE_LEGACY from '@data/bundle-legacy'; import allCourseContent from '@data/courses'; import storefront from '@data/course-storefront'; diff --git a/src/services/discount/index.ts b/src/services/discount/index.ts index 5509c0fc..6c2244e9 100644 --- a/src/services/discount/index.ts +++ b/src/services/discount/index.ts @@ -1,7 +1,7 @@ import axios from 'axios'; -import { COURSE } from '@data/course-keys'; -import { BUNDLE } from '@data/bundle-keys'; +import { COURSE } from '@data/course-keys-types'; +import { BUNDLE } from '@data/bundle-keys-types'; import { getUpgradeableCourses } from '@services/course'; import { Course } from '@models/course'; diff --git a/src/services/firebase/course.ts b/src/services/firebase/course.ts index f7b44b97..510b866b 100644 --- a/src/services/firebase/course.ts +++ b/src/services/firebase/course.ts @@ -2,8 +2,8 @@ import * as firebaseAdminVanilla from 'firebase-admin'; import firebaseAdmin from '@services/firebase/admin'; -import { COURSE } from '@data/course-keys'; -import { BUNDLE } from '@data/bundle-keys'; +import { COURSE } from '@data/course-keys-types'; +import { BUNDLE } from '@data/bundle-keys-types'; export const createCourse = async ({ uid, diff --git a/src/types/storefront.ts b/src/types/storefront.ts deleted file mode 100644 index b7b8cfb5..00000000 --- a/src/types/storefront.ts +++ /dev/null @@ -1,15 +0,0 @@ -export type Storefront = { - course: StorefrontCourse; -} | null; - -type StorefrontCourse = { - header: string; - courseId: string; - bundle: StorefrontBundle; -}; - -type StorefrontBundle = { - header: string; - bundleId: string; - price: number; -}; From 6503542223d53585bdb1501497824f4822a6b030 Mon Sep 17 00:00:00 2001 From: Robin Wieruch Date: Thu, 16 Apr 2020 10:27:33 +0200 Subject: [PATCH 06/13] sentry middleware --- pages/api/graphql.ts | 7 ++- src/api/middleware/{getMe.ts => me.ts} | 1 + src/api/middleware/sentry.ts | 27 ++++++++++++ src/api/schema/index.ts | 26 ------------ src/api/typeDefs/index.ts | 59 -------------------------- 5 files changed, 33 insertions(+), 87 deletions(-) rename src/api/middleware/{getMe.ts => me.ts} (92%) create mode 100644 src/api/middleware/sentry.ts delete mode 100644 src/api/typeDefs/index.ts diff --git a/pages/api/graphql.ts b/pages/api/graphql.ts index fab9828b..608679a5 100644 --- a/pages/api/graphql.ts +++ b/pages/api/graphql.ts @@ -1,6 +1,7 @@ import { ApolloServer } from 'apollo-server-micro'; import cors from 'micro-cors'; import { buildSchema } from 'type-graphql'; +import { applyMiddleware } from 'graphql-middleware'; import 'reflect-metadata'; import getConnection from '@models/index'; @@ -12,7 +13,8 @@ import { ServerRequest, ServerResponse } from '@typeDefs/server'; import { ResolverContext } from '@typeDefs/resolver'; import resolvers from '@api/resolvers'; -import getMe from '@api/middleware/getMe'; +import getMe from '@api/middleware/me'; +import sentryMiddleware from '@api/middleware/sentry'; import firebaseAdmin from '@services/firebase/admin'; if (process.env.FIREBASE_ADMIN_UID) { @@ -50,7 +52,8 @@ export default async (req: ServerRequest, res: ServerResponse) => { }); const server = new ApolloServer({ - schema, + schema: applyMiddleware(schema, sentryMiddleware), + context: async ({ req, res }): Promise => { const me = await getMe(req, res); diff --git a/src/api/middleware/getMe.ts b/src/api/middleware/me.ts similarity index 92% rename from src/api/middleware/getMe.ts rename to src/api/middleware/me.ts index 97e093cd..f92fc930 100644 --- a/src/api/middleware/getMe.ts +++ b/src/api/middleware/me.ts @@ -2,6 +2,7 @@ import { AuthenticationError } from 'apollo-server-micro'; import firebaseAdmin from '@services/firebase/admin'; +import { ResolverContext } from '@typeDefs/resolver'; import { ServerResponse, ServerRequest } from '@typeDefs/server'; import { User } from '@typeDefs/user'; diff --git a/src/api/middleware/sentry.ts b/src/api/middleware/sentry.ts new file mode 100644 index 00000000..e42874d2 --- /dev/null +++ b/src/api/middleware/sentry.ts @@ -0,0 +1,27 @@ +import { sentry } from 'graphql-middleware-sentry'; +import * as Sentry from '@sentry/node'; + +import { ResolverContext } from '@typeDefs/resolver'; + +Sentry.init({ + dsn: process.env.SENTRY_DSN, +}); + +export default sentry({ + sentryInstance: Sentry, + config: { + environment: process.env.NODE_ENV, + }, + forwardErrors: true, + captureReturnedErrors: true, + withScope: (scope, error, context: ResolverContext) => { + scope.setUser({ + id: context.me?.uid, + email: context.me?.email, + }); + + scope.setExtra('body', context.req.body); + scope.setExtra('origin', context.req.headers.origin); + scope.setExtra('user-agent', context.req.headers['user-agent']); + }, +}); diff --git a/src/api/schema/index.ts b/src/api/schema/index.ts index b83516c8..c799eb48 100644 --- a/src/api/schema/index.ts +++ b/src/api/schema/index.ts @@ -1,33 +1,8 @@ // import { mergeSchemas } from 'graphql-tools'; // import { applyMiddleware } from 'graphql-middleware'; -// import { sentry } from 'graphql-middleware-sentry'; -// import * as Sentry from '@sentry/node'; // import { ResolverContext } from '@typeDefs/resolver'; -// Sentry.init({ -// dsn: process.env.SENTRY_DSN, -// }); - -// const sentryMiddleware = sentry({ -// sentryInstance: Sentry, -// config: { -// environment: process.env.NODE_ENV, -// }, -// forwardErrors: true, -// captureReturnedErrors: true, -// withScope: (scope, error, context: ResolverContext) => { -// scope.setUser({ -// id: context.me?.uid, -// email: context.me?.email, -// }); - -// scope.setExtra('body', context.req.body); -// scope.setExtra('origin', context.req.headers.origin); -// scope.setExtra('user-agent', context.req.headers['user-agent']); -// }, -// }); - // import { Resolvers } from '@generated/server'; // import authorization from '@api/authorization'; @@ -42,5 +17,4 @@ // export default applyMiddleware( // schema, // authorization, -// sentryMiddleware // ); diff --git a/src/api/typeDefs/index.ts b/src/api/typeDefs/index.ts deleted file mode 100644 index 60b0b570..00000000 --- a/src/api/typeDefs/index.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { gql } from 'apollo-server-express'; - -import mirgationSchema from './migration'; -import sessionSchema from './session'; -import userSchema from './user'; -import storefrontSchema from './storefront'; -import paypalSchema from './paypal'; -import stripeSchema from './stripe'; -import courseSchema from './course'; -import bookSchema from './book'; -import upgradeSchema from './upgrade'; -import couponSchema from './coupon'; -import partnerSchema from './partner'; -import communitySchema from './community'; - -const linkSchema = gql` - scalar DateTime - - enum CourseId { - THE_ROAD_TO_LEARN_REACT - TAMING_THE_STATE - THE_ROAD_TO_GRAPHQL - THE_ROAD_TO_REACT_WITH_FIREBASE - } - - enum BundleId { - STUDENT - INTERMEDIATE - PROFESSIONAL - } - - type Query { - _: Boolean - } - - type Mutation { - _: Boolean - } - - type Subscription { - _: Boolean - } -`; - -export default [ - linkSchema, - mirgationSchema, - sessionSchema, - userSchema, - storefrontSchema, - paypalSchema, - stripeSchema, - courseSchema, - bookSchema, - upgradeSchema, - couponSchema, - partnerSchema, - communitySchema, -]; From c9adeaa626541c2a02a370457542f9412d81492f Mon Sep 17 00:00:00 2001 From: Robin Wieruch Date: Thu, 16 Apr 2020 11:00:08 +0200 Subject: [PATCH 07/13] getMe as middleware --- pages/api/graphql.ts | 14 ++++++-------- src/api/middleware/me.ts | 17 +++++++++++++---- src/api/resolvers/course/index.ts | 6 +++--- src/api/resolvers/index.ts | 4 +++- src/models/index.ts | 8 ++------ 5 files changed, 27 insertions(+), 22 deletions(-) diff --git a/pages/api/graphql.ts b/pages/api/graphql.ts index 608679a5..cebe9040 100644 --- a/pages/api/graphql.ts +++ b/pages/api/graphql.ts @@ -1,7 +1,9 @@ import { ApolloServer } from 'apollo-server-micro'; -import cors from 'micro-cors'; import { buildSchema } from 'type-graphql'; import { applyMiddleware } from 'graphql-middleware'; + +import cors from 'micro-cors'; + import 'reflect-metadata'; import getConnection from '@models/index'; @@ -13,8 +15,9 @@ import { ServerRequest, ServerResponse } from '@typeDefs/server'; import { ResolverContext } from '@typeDefs/resolver'; import resolvers from '@api/resolvers'; -import getMe from '@api/middleware/me'; +import meMiddleware from '@api/middleware/me'; import sentryMiddleware from '@api/middleware/sentry'; + import firebaseAdmin from '@services/firebase/admin'; if (process.env.FIREBASE_ADMIN_UID) { @@ -52,11 +55,9 @@ export default async (req: ServerRequest, res: ServerResponse) => { }); const server = new ApolloServer({ - schema: applyMiddleware(schema, sentryMiddleware), + schema: applyMiddleware(schema, sentryMiddleware, meMiddleware), context: async ({ req, res }): Promise => { - const me = await getMe(req, res); - const adminConnector = new AdminConnector(); const partnerConnector = new PartnerConnector(connection!); const courseConnector = new CourseConnector(connection!); @@ -65,7 +66,6 @@ export default async (req: ServerRequest, res: ServerResponse) => { return { req, res, - me, adminConnector, courseConnector, partnerConnector, @@ -78,7 +78,5 @@ export default async (req: ServerRequest, res: ServerResponse) => { server.createHandler({ path: '/api/graphql' }) ); - // await connection.close(); - return handler(req, res); }; diff --git a/src/api/middleware/me.ts b/src/api/middleware/me.ts index f92fc930..e72497bc 100644 --- a/src/api/middleware/me.ts +++ b/src/api/middleware/me.ts @@ -3,11 +3,16 @@ import { AuthenticationError } from 'apollo-server-micro'; import firebaseAdmin from '@services/firebase/admin'; import { ResolverContext } from '@typeDefs/resolver'; -import { ServerResponse, ServerRequest } from '@typeDefs/server'; import { User } from '@typeDefs/user'; -export default async (req: ServerRequest, res: ServerResponse) => { - const { session } = req.cookies; +export default async ( + resolve: Function, + root: any, + args: any, + context: ResolverContext, + info: any +) => { + const { session } = context.req.cookies; if (!session) { return undefined; @@ -15,7 +20,7 @@ export default async (req: ServerRequest, res: ServerResponse) => { const CHECK_REVOKED = true; - return await firebaseAdmin + const me = await firebaseAdmin .auth() .verifySessionCookie(session, CHECK_REVOKED) .then(async claims => { @@ -24,4 +29,8 @@ export default async (req: ServerRequest, res: ServerResponse) => { .catch(error => { throw new AuthenticationError(error.message); }); + + context.me = me; + + return await resolve(root, args, context, info); }; diff --git a/src/api/resolvers/course/index.ts b/src/api/resolvers/course/index.ts index 16746d2b..2d4e6a09 100644 --- a/src/api/resolvers/course/index.ts +++ b/src/api/resolvers/course/index.ts @@ -233,7 +233,7 @@ export default class CourseResolver { async unlockedCourse( @Arg('courseId') courseId: string, @Ctx() ctx: ResolverContext - ) { + ): Promise { if (!ctx.me) { return null; } @@ -257,7 +257,7 @@ export default class CourseResolver { @Arg('courseId') courseId: string, @Arg('bundleId') bundleId: string, @Ctx() ctx: ResolverContext - ) { + ): Promise { if (!ctx.me) { return false; } @@ -292,7 +292,7 @@ export default class CourseResolver { @Arg('bundleId') bundleId: string, @Arg('uid') uid: string, @Ctx() ctx: ResolverContext - ) { + ): Promise { await ctx.courseConnector.createCourse({ userId: uid, courseId: courseId as COURSE, diff --git a/src/api/resolvers/index.ts b/src/api/resolvers/index.ts index 1dbefb7a..ca63bee4 100644 --- a/src/api/resolvers/index.ts +++ b/src/api/resolvers/index.ts @@ -1,3 +1,5 @@ +import { NonEmptyArray } from 'type-graphql'; + import MigrationResolvers from './migration'; import SessionResolver from './session'; import UserResolvers from './user'; @@ -24,4 +26,4 @@ export default [ CouponResolver, PartnerResolver, CommunityResolvers, -]; +] as NonEmptyArray; diff --git a/src/models/index.ts b/src/models/index.ts index 2d763ad5..7c07ecdf 100644 --- a/src/models/index.ts +++ b/src/models/index.ts @@ -13,9 +13,7 @@ export default async function() { if (connection.isConnected) { await connection.close(); } - } catch (error) { - console.log(error); - } + } catch (error) {} try { connection = await createConnection({ @@ -38,9 +36,7 @@ export default async function() { }, }), }); - } catch (error) { - console.log(error); - } + } catch (error) {} return connection; } From 52d9f99715727d4782a9bdd99b792ef90508e356 Mon Sep 17 00:00:00 2001 From: Robin Wieruch Date: Thu, 16 Apr 2020 12:07:00 +0200 Subject: [PATCH 08/13] session resolver authorization --- pages/api/graphql.ts | 4 +- src/api/authorization/index.ts | 3 +- src/api/middleware/{ => global}/me.ts | 2 +- src/api/middleware/{ => global}/sentry.ts | 0 .../middleware/resolver/isAuthenticated.ts | 15 +++++ src/api/resolvers/session/index.ts | 64 +++++++++++-------- src/generated/client.tsx | 16 ++--- src/queries/session.ts | 4 +- .../PasswordChangeForm/index.tsx | 3 - src/screens/SignIn/SignInForm/index.tsx | 2 +- src/screens/SignIn/SignInForm/spec.tsx | 2 +- src/screens/SignUp/SignUpForm/index.tsx | 2 +- src/screens/SignUp/SignUpForm/spec.tsx | 2 +- 13 files changed, 72 insertions(+), 47 deletions(-) rename src/api/middleware/{ => global}/me.ts (93%) rename src/api/middleware/{ => global}/sentry.ts (100%) create mode 100644 src/api/middleware/resolver/isAuthenticated.ts diff --git a/pages/api/graphql.ts b/pages/api/graphql.ts index cebe9040..9c92af4c 100644 --- a/pages/api/graphql.ts +++ b/pages/api/graphql.ts @@ -15,8 +15,8 @@ import { ServerRequest, ServerResponse } from '@typeDefs/server'; import { ResolverContext } from '@typeDefs/resolver'; import resolvers from '@api/resolvers'; -import meMiddleware from '@api/middleware/me'; -import sentryMiddleware from '@api/middleware/sentry'; +import meMiddleware from '@api/middleware/global/me'; +import sentryMiddleware from '@api/middleware/global/sentry'; import firebaseAdmin from '@services/firebase/admin'; diff --git a/src/api/authorization/index.ts b/src/api/authorization/index.ts index 2fd3ac1f..a00b8bac 100644 --- a/src/api/authorization/index.ts +++ b/src/api/authorization/index.ts @@ -17,7 +17,8 @@ export default shield({ partnerPayments: and(isAuthenticated, isPartner), }, Mutation: { - passwordChange: isAuthenticated, + // passwordChange: isAuthenticated, + // emailChange: isAuthenticated, communityJoin: isAuthenticated, // Admin diff --git a/src/api/middleware/me.ts b/src/api/middleware/global/me.ts similarity index 93% rename from src/api/middleware/me.ts rename to src/api/middleware/global/me.ts index e72497bc..bb73b495 100644 --- a/src/api/middleware/me.ts +++ b/src/api/middleware/global/me.ts @@ -15,7 +15,7 @@ export default async ( const { session } = context.req.cookies; if (!session) { - return undefined; + return await resolve(root, args, context, info); } const CHECK_REVOKED = true; diff --git a/src/api/middleware/sentry.ts b/src/api/middleware/global/sentry.ts similarity index 100% rename from src/api/middleware/sentry.ts rename to src/api/middleware/global/sentry.ts diff --git a/src/api/middleware/resolver/isAuthenticated.ts b/src/api/middleware/resolver/isAuthenticated.ts new file mode 100644 index 00000000..61eb28fb --- /dev/null +++ b/src/api/middleware/resolver/isAuthenticated.ts @@ -0,0 +1,15 @@ +import { MiddlewareFn } from 'type-graphql'; +import { ForbiddenError } from 'apollo-server'; + +import { ResolverContext } from '@typeDefs/resolver'; + +export const isAuthenticated: MiddlewareFn = async ( + { context }, + next +) => { + if (!context.me) { + return new ForbiddenError('Not authenticated as user.'); + } + + return next(); +}; diff --git a/src/api/resolvers/session/index.ts b/src/api/resolvers/session/index.ts index 7f1c0937..00d112c0 100644 --- a/src/api/resolvers/session/index.ts +++ b/src/api/resolvers/session/index.ts @@ -5,6 +5,7 @@ import { Ctx, Resolver, Mutation, + UseMiddleware, } from 'type-graphql'; import { ResolverContext } from '@typeDefs/resolver'; @@ -13,11 +14,12 @@ import firebase from '@services/firebase/client'; import firebaseAdmin from '@services/firebase/admin'; import { inviteToRevue } from '@services/revue'; import { inviteToConvertkit } from '@services/convertkit'; +import { isAuthenticated } from '@api/middleware/resolver/isAuthenticated'; @ObjectType() class SessionToken { @Field() - sessionToken: string; + token: string; } @Resolver() @@ -26,7 +28,7 @@ export default class SessionResolver { async signIn( @Arg('email') email: string, @Arg('password') password: string - ) { + ): Promise { let result; try { @@ -34,20 +36,29 @@ export default class SessionResolver { .auth() .signInWithEmailAndPassword(email, password); } catch (error) { - return new Error(error); + throw new Error(error); + } + + if (!result.user) { + throw new Error('No user found.'); } - const idToken = await result.user?.getIdToken(); - const sessionToken = await firebaseAdmin + const idToken = await result.user.getIdToken(); + + const token = await firebaseAdmin .auth() - .createSessionCookie(idToken || '', { + .createSessionCookie(idToken, { expiresIn: EXPIRES_IN, }); + if (!token) { + throw new Error('Not able to create a session cookie.'); + } + // We manage the session ourselves. await firebase.auth().signOut(); - return { sessionToken }; + return { token }; } @Mutation(() => SessionToken) @@ -63,23 +74,22 @@ export default class SessionResolver { displayName: username, }); } catch (error) { - if (error.message.includes('email address is already in use')) { - return new Error( - 'You already registered with this email. Hint: Check your password manager for our old domain: roadtoreact.com' - ); - } else { - return new Error(error); - } + throw new Error(error); } const { user } = await firebase .auth() .signInWithEmailAndPassword(email, password); - const idToken = await user?.getIdToken(); - const sessionToken = await firebaseAdmin + if (!user) { + throw new Error('No user found.'); + } + + const idToken = await user.getIdToken(); + + const token = await firebaseAdmin .auth() - .createSessionCookie(idToken || '', { + .createSessionCookie(idToken, { expiresIn: EXPIRES_IN, }); @@ -98,47 +108,49 @@ export default class SessionResolver { console.log(error); } - return { sessionToken }; + return { token }; } - @Mutation(() => Boolean, { nullable: true }) + @Mutation(() => Boolean) async passwordForgot(@Arg('email') email: string) { try { await firebase.auth().sendPasswordResetEmail(email); } catch (error) { - return new Error(error); + throw new Error(error); } return true; } - @Mutation(() => Boolean, { nullable: true }) + @Mutation(() => Boolean) + @UseMiddleware(isAuthenticated) async passwordChange( @Arg('password') password: string, @Ctx() ctx: ResolverContext ) { try { - await firebaseAdmin.auth().updateUser(ctx.me?.uid || '', { + await firebaseAdmin.auth().updateUser(ctx.me!.uid, { password, }); } catch (error) { - return new Error(error); + throw new Error(error); } return true; } - @Mutation(() => Boolean, { nullable: true }) + @Mutation(() => Boolean) + @UseMiddleware(isAuthenticated) async emailChange( @Arg('email') email: string, @Ctx() ctx: ResolverContext ) { try { - await firebaseAdmin.auth().updateUser(ctx.me?.uid || '', { + await firebaseAdmin.auth().updateUser(ctx.me!.uid, { email, }); } catch (error) { - return new Error(error); + throw new Error(error); } return true; diff --git a/src/generated/client.tsx b/src/generated/client.tsx index 4ac48625..6347986e 100644 --- a/src/generated/client.tsx +++ b/src/generated/client.tsx @@ -121,9 +121,9 @@ export type Mutation = { migrate: Scalars['Boolean']; signIn: SessionToken; signUp: SessionToken; - passwordForgot?: Maybe; - passwordChange?: Maybe; - emailChange?: Maybe; + passwordForgot: Scalars['Boolean']; + passwordChange: Scalars['Boolean']; + emailChange: Scalars['Boolean']; paypalCreateOrder: PaypalOrderId; paypalApproveOrder: Scalars['Boolean']; stripeCreateOrder: StripeId; @@ -347,7 +347,7 @@ export type QueryPartnerSalesArgs = { export type SessionToken = { __typename?: 'SessionToken'; - sessionToken: Scalars['String']; + token: Scalars['String']; }; export type StorefrontBundle = { @@ -682,7 +682,7 @@ export type SignUpMutation = ( { __typename?: 'Mutation' } & { signUp: ( { __typename?: 'SessionToken' } - & Pick + & Pick ) } ); @@ -696,7 +696,7 @@ export type SignInMutation = ( { __typename?: 'Mutation' } & { signIn: ( { __typename?: 'SessionToken' } - & Pick + & Pick ) } ); @@ -1435,7 +1435,7 @@ export type PaypalApproveOrderMutationOptions = ApolloReactCommon.BaseMutationOp export const SignUpDocument = gql` mutation SignUp($username: String!, $email: String!, $password: String!) { signUp(username: $username, email: $email, password: $password) { - sessionToken + token } } `; @@ -1469,7 +1469,7 @@ export type SignUpMutationOptions = ApolloReactCommon.BaseMutationOptions { const { successMessage } = useIndicators({ key: 'password-change', error, - success: { - message: 'Success! Check your email inbox.', - }, }); const [ diff --git a/src/screens/SignIn/SignInForm/index.tsx b/src/screens/SignIn/SignInForm/index.tsx index dce538c4..3df8e77b 100644 --- a/src/screens/SignIn/SignInForm/index.tsx +++ b/src/screens/SignIn/SignInForm/index.tsx @@ -57,7 +57,7 @@ const SignInForm = ({ }, }); - cookie.set('session', data?.signIn.sessionToken || '', { + cookie.set('session', data?.signIn.token || '', { expires: EXPIRES_IN, // TODO: 1) Get it work with httpOnly 2) Get it work on the server. See SignUpForm.tsx // httpOnly: true, diff --git a/src/screens/SignIn/SignInForm/spec.tsx b/src/screens/SignIn/SignInForm/spec.tsx index e3ae70ae..c2dc3b45 100644 --- a/src/screens/SignIn/SignInForm/spec.tsx +++ b/src/screens/SignIn/SignInForm/spec.tsx @@ -29,7 +29,7 @@ describe('SignInForm', () => { }, result: () => { mutationCalled = true; - return { data: { signIn: { sessionToken: '1' } } }; + return { data: { signIn: { token: '1' } } }; }, }, ]; diff --git a/src/screens/SignUp/SignUpForm/index.tsx b/src/screens/SignUp/SignUpForm/index.tsx index 9de7772b..e48f3fd9 100644 --- a/src/screens/SignUp/SignUpForm/index.tsx +++ b/src/screens/SignUp/SignUpForm/index.tsx @@ -82,7 +82,7 @@ const SignUpForm = ({ }, }); - cookie.set('session', data?.signUp.sessionToken || '', { + cookie.set('session', data?.signUp.token || '', { expires: EXPIRES_IN, // TODO: 1) Get it work with httpOnly 2) Get it work on the server. See SignUpForm.tsx // httpOnly: true, diff --git a/src/screens/SignUp/SignUpForm/spec.tsx b/src/screens/SignUp/SignUpForm/spec.tsx index 1dc6da00..6f62c460 100644 --- a/src/screens/SignUp/SignUpForm/spec.tsx +++ b/src/screens/SignUp/SignUpForm/spec.tsx @@ -30,7 +30,7 @@ describe('SignUpForm', () => { }, result: () => { mutationCalled = true; - return { data: { signUp: { sessionToken: '1' } } }; + return { data: { signUp: { token: '1' } } }; }, }, ]; From 1551580b671cd5e2c1a392ad1e19a3d0c87dd1bc Mon Sep 17 00:00:00 2001 From: Robin Wieruch Date: Thu, 16 Apr 2020 15:04:03 +0200 Subject: [PATCH 09/13] coupon resolver authorization --- src/api/authorization/index.ts | 4 ++-- src/api/middleware/resolver/isAdmin.ts | 20 +++++++++++++++++++ .../middleware/resolver/isAuthenticated.ts | 2 +- src/api/resolvers/coupon/index.ts | 17 ++++++++-------- src/api/resolvers/session/index.ts | 10 ++++++---- src/api/resolvers/user/index.ts | 18 ++++++++++------- src/validation/admin.ts | 2 +- 7 files changed, 50 insertions(+), 23 deletions(-) create mode 100644 src/api/middleware/resolver/isAdmin.ts diff --git a/src/api/authorization/index.ts b/src/api/authorization/index.ts index a00b8bac..063b3e42 100644 --- a/src/api/authorization/index.ts +++ b/src/api/authorization/index.ts @@ -7,8 +7,8 @@ import { isFreeCourse } from './isFreeCourse'; export default shield({ Query: { - me: isAuthenticated, - discountedPrice: isAuthenticated, + // me: isAuthenticated, + // discountedPrice: isAuthenticated, // Partner diff --git a/src/api/middleware/resolver/isAdmin.ts b/src/api/middleware/resolver/isAdmin.ts new file mode 100644 index 00000000..87cc8279 --- /dev/null +++ b/src/api/middleware/resolver/isAdmin.ts @@ -0,0 +1,20 @@ +import { MiddlewareFn } from 'type-graphql'; +import { ForbiddenError } from 'apollo-server'; + +import { ResolverContext } from '@typeDefs/resolver'; +import { hasAdminRole } from '@validation/admin'; + +export const isAdmin: MiddlewareFn = async ( + { context }, + next +) => { + if (!context.me) { + throw new ForbiddenError('Not authenticated as user.'); + } + + if (!hasAdminRole(context.me)) { + throw new ForbiddenError('No admin user.'); + } + + return next(); +}; diff --git a/src/api/middleware/resolver/isAuthenticated.ts b/src/api/middleware/resolver/isAuthenticated.ts index 61eb28fb..86026e99 100644 --- a/src/api/middleware/resolver/isAuthenticated.ts +++ b/src/api/middleware/resolver/isAuthenticated.ts @@ -8,7 +8,7 @@ export const isAuthenticated: MiddlewareFn = async ( next ) => { if (!context.me) { - return new ForbiddenError('Not authenticated as user.'); + throw new ForbiddenError('Not authenticated as user.'); } return next(); diff --git a/src/api/resolvers/coupon/index.ts b/src/api/resolvers/coupon/index.ts index b8f38bd1..12528f2c 100644 --- a/src/api/resolvers/coupon/index.ts +++ b/src/api/resolvers/coupon/index.ts @@ -6,6 +6,7 @@ import { Resolver, Query, Mutation, + UseMiddleware, } from 'type-graphql'; import { ResolverContext } from '@typeDefs/resolver'; @@ -13,6 +14,8 @@ import { priceWithDiscount } from '@services/discount'; import storefront from '@data/course-storefront'; import { COURSE } from '@data/course-keys-types'; import { BUNDLE } from '@data/bundle-keys-types'; +import { isAuthenticated } from '@api/middleware/resolver/isAuthenticated'; +import { isAdmin } from '@api/middleware/resolver/isAdmin'; @ObjectType() class Discount { @Field() @@ -25,19 +28,16 @@ class Discount { @Resolver() export default class CouponResolver { @Query(() => Discount) + @UseMiddleware(isAuthenticated) async discountedPrice( @Arg('courseId') courseId: string, @Arg('bundleId') bundleId: string, @Arg('coupon') coupon: string, @Ctx() ctx: ResolverContext - ) { + ): Promise { const course = storefront[courseId as COURSE]; const bundle = course.bundles[bundleId as BUNDLE]; - if (!ctx.me) { - return bundle.price; - } - const price = await priceWithDiscount( ctx.couponConnector, ctx.courseConnector @@ -46,7 +46,7 @@ export default class CouponResolver { bundleId as BUNDLE, bundle.price, coupon, - ctx.me.uid + ctx.me!.uid ); return { @@ -55,13 +55,14 @@ export default class CouponResolver { }; } - @Mutation(() => Boolean, { nullable: true }) + @Mutation(() => Boolean) + @UseMiddleware(isAuthenticated, isAdmin) async couponCreate( @Arg('coupon') coupon: string, @Arg('discount') discount: number, @Arg('count') count: number, @Ctx() ctx: ResolverContext - ) { + ): Promise { try { await ctx.couponConnector.createCoupons( coupon, diff --git a/src/api/resolvers/session/index.ts b/src/api/resolvers/session/index.ts index 00d112c0..8c727b83 100644 --- a/src/api/resolvers/session/index.ts +++ b/src/api/resolvers/session/index.ts @@ -66,7 +66,7 @@ export default class SessionResolver { @Arg('username') username: string, @Arg('email') email: string, @Arg('password') password: string - ) { + ): Promise { try { await firebaseAdmin.auth().createUser({ email, @@ -112,7 +112,9 @@ export default class SessionResolver { } @Mutation(() => Boolean) - async passwordForgot(@Arg('email') email: string) { + async passwordForgot( + @Arg('email') email: string + ): Promise { try { await firebase.auth().sendPasswordResetEmail(email); } catch (error) { @@ -127,7 +129,7 @@ export default class SessionResolver { async passwordChange( @Arg('password') password: string, @Ctx() ctx: ResolverContext - ) { + ): Promise { try { await firebaseAdmin.auth().updateUser(ctx.me!.uid, { password, @@ -144,7 +146,7 @@ export default class SessionResolver { async emailChange( @Arg('email') email: string, @Ctx() ctx: ResolverContext - ) { + ): Promise { try { await firebaseAdmin.auth().updateUser(ctx.me!.uid, { email, diff --git a/src/api/resolvers/user/index.ts b/src/api/resolvers/user/index.ts index 6794fda9..3f0a4707 100644 --- a/src/api/resolvers/user/index.ts +++ b/src/api/resolvers/user/index.ts @@ -4,17 +4,19 @@ import { Ctx, Resolver, Query, + UseMiddleware, } from 'type-graphql'; import { ResolverContext } from '@typeDefs/resolver'; +import { isAuthenticated } from '@api/middleware/resolver/isAuthenticated'; @ObjectType() class User { @Field() - email: string; + uid: string; @Field() - uid: string; + email: string; @Field() username: string; @@ -26,16 +28,18 @@ class User { @Resolver() export default class UserResolver { @Query(() => User) - async me(@Ctx() ctx: ResolverContext) { - const rolesObject = ctx.me?.customClaims || {}; + @UseMiddleware(isAuthenticated) + async me(@Ctx() ctx: ResolverContext): Promise { + const rolesObject = ctx.me!.customClaims || {}; + const roles = Object.keys(rolesObject).filter( key => rolesObject[key] ); return { - email: ctx.me?.email, - uid: ctx.me?.uid, - username: ctx.me?.displayName, + uid: ctx.me!.uid, + email: ctx.me!.email || '', + username: ctx.me!.displayName || '', roles, }; } diff --git a/src/validation/admin.ts b/src/validation/admin.ts index a3c809cf..cb05e9d5 100644 --- a/src/validation/admin.ts +++ b/src/validation/admin.ts @@ -1,5 +1,5 @@ import * as ROLES from '@constants/roles'; import { User } from '@typeDefs/user'; -export const hasAdminRole = (user: User) => +export const hasAdminRole = (user: User | null | undefined) => user && user.customClaims && user.customClaims[ROLES.ADMIN]; From 38d1c0c55f13940fdd2caaa8998e388ecd499ec4 Mon Sep 17 00:00:00 2001 From: Robin Wieruch Date: Thu, 16 Apr 2020 15:35:38 +0200 Subject: [PATCH 10/13] rest authorization --- src/api/authorization/index.ts | 35 --------------------- src/api/authorization/isAdmin.ts | 14 --------- src/api/authorization/isAuthenticated.ts | 6 ---- src/api/authorization/isFreeCourse.ts | 19 ----------- src/api/authorization/isPartner.ts | 14 --------- src/api/middleware/resolver/isFreeCourse.ts | 20 ++++++++++++ src/api/middleware/resolver/isPartner.ts | 20 ++++++++++++ src/api/resolvers/book/index.ts | 26 ++++++++++----- src/api/resolvers/community/index.ts | 12 ++++--- src/api/resolvers/course/index.ts | 17 +++++----- src/api/resolvers/migration/index.ts | 9 ++++-- src/api/resolvers/partner/index.ts | 34 ++++++++++---------- src/api/resolvers/storefront/index.ts | 10 +++--- src/api/resolvers/stripe/index.ts | 2 ++ src/api/resolvers/upgrade/index.ts | 16 ++++++---- src/generated/client.tsx | 10 +++--- 16 files changed, 124 insertions(+), 140 deletions(-) delete mode 100644 src/api/authorization/index.ts delete mode 100644 src/api/authorization/isAdmin.ts delete mode 100644 src/api/authorization/isAuthenticated.ts delete mode 100644 src/api/authorization/isFreeCourse.ts delete mode 100644 src/api/authorization/isPartner.ts create mode 100644 src/api/middleware/resolver/isFreeCourse.ts create mode 100644 src/api/middleware/resolver/isPartner.ts diff --git a/src/api/authorization/index.ts b/src/api/authorization/index.ts deleted file mode 100644 index 063b3e42..00000000 --- a/src/api/authorization/index.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { shield, and } from 'graphql-shield'; - -import { isAuthenticated } from './isAuthenticated'; -import { isAdmin } from './isAdmin'; -import { isPartner } from './isPartner'; -import { isFreeCourse } from './isFreeCourse'; - -export default shield({ - Query: { - // me: isAuthenticated, - // discountedPrice: isAuthenticated, - - // Partner - - partnerVisitors: and(isAuthenticated, isPartner), - partnerSales: and(isAuthenticated, isPartner), - partnerPayments: and(isAuthenticated, isPartner), - }, - Mutation: { - // passwordChange: isAuthenticated, - // emailChange: isAuthenticated, - communityJoin: isAuthenticated, - - // Admin - - migrate: and(isAuthenticated, isAdmin), - promoteToPartner: and(isAuthenticated, isAdmin), - couponCreate: and(isAuthenticated, isAdmin), - createAdminCourse: and(isAuthenticated, isAdmin), - - // Free - - createFreeCourse: and(isAuthenticated, isFreeCourse), - }, -}); diff --git a/src/api/authorization/isAdmin.ts b/src/api/authorization/isAdmin.ts deleted file mode 100644 index 86c3bb2f..00000000 --- a/src/api/authorization/isAdmin.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { rule } from 'graphql-shield'; -import { ForbiddenError } from 'apollo-server'; - -import { hasAdminRole } from '@validation/admin'; - -export const isAdmin = rule()(async (_, __, { me }) => { - if (!me) { - return new ForbiddenError('Not authenticated as user.'); - } - - return hasAdminRole(me) - ? true - : new ForbiddenError('No admin user.'); -}); diff --git a/src/api/authorization/isAuthenticated.ts b/src/api/authorization/isAuthenticated.ts deleted file mode 100644 index 038a336e..00000000 --- a/src/api/authorization/isAuthenticated.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { rule } from 'graphql-shield'; -import { ForbiddenError } from 'apollo-server'; - -export const isAuthenticated = rule()(async (_, __, { me }) => { - return me ? true : new ForbiddenError('Not authenticated as user.'); -}); diff --git a/src/api/authorization/isFreeCourse.ts b/src/api/authorization/isFreeCourse.ts deleted file mode 100644 index 6944e98a..00000000 --- a/src/api/authorization/isFreeCourse.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { rule } from 'graphql-shield'; - -import storefront from '@data/course-storefront'; -import { COURSE } from '@data/course-keys-types'; -import { BUNDLE } from '@data/bundle-keys-types'; - -export const isFreeCourse = rule()( - async ( - _, - { courseId, bundleId }: { courseId: COURSE; bundleId: BUNDLE } - ) => { - const course = storefront[courseId]; - const bundle = course.bundles[bundleId]; - - return bundle.price === 0 - ? true - : new Error('This course is not for free.'); - } -); diff --git a/src/api/authorization/isPartner.ts b/src/api/authorization/isPartner.ts deleted file mode 100644 index 787ee7e2..00000000 --- a/src/api/authorization/isPartner.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { rule } from 'graphql-shield'; -import { ForbiddenError } from 'apollo-server'; - -import { hasPartnerRole } from '@validation/partner'; - -export const isPartner = rule()(async (_, __, { me }) => { - if (!me) { - return new ForbiddenError('Not authenticated as user.'); - } - - return hasPartnerRole(me) - ? true - : new ForbiddenError('No partner user.'); -}); diff --git a/src/api/middleware/resolver/isFreeCourse.ts b/src/api/middleware/resolver/isFreeCourse.ts new file mode 100644 index 00000000..1a1fdb2a --- /dev/null +++ b/src/api/middleware/resolver/isFreeCourse.ts @@ -0,0 +1,20 @@ +import { MiddlewareFn } from 'type-graphql'; + +import { ResolverContext } from '@typeDefs/resolver'; +import storefront from '@data/course-storefront'; +import { COURSE } from '@data/course-keys-types'; +import { BUNDLE } from '@data/bundle-keys-types'; + +export const isFreeCourse: MiddlewareFn = async ( + { args }, + next +) => { + const course = storefront[args.courseId as COURSE]; + const bundle = course.bundles[args.bundleId as BUNDLE]; + + if (bundle.price !== 0) { + throw new Error('This course is not for free.'); + } + + return next(); +}; diff --git a/src/api/middleware/resolver/isPartner.ts b/src/api/middleware/resolver/isPartner.ts new file mode 100644 index 00000000..dcfbe3a7 --- /dev/null +++ b/src/api/middleware/resolver/isPartner.ts @@ -0,0 +1,20 @@ +import { MiddlewareFn } from 'type-graphql'; +import { ForbiddenError } from 'apollo-server'; + +import { ResolverContext } from '@typeDefs/resolver'; +import { hasPartnerRole } from '@validation/partner'; + +export const isPartner: MiddlewareFn = async ( + { context }, + next +) => { + if (!context.me) { + throw new ForbiddenError('Not authenticated as user.'); + } + + if (!hasPartnerRole(context.me)) { + throw new Error('No partner user.'); + } + + return next(); +}; diff --git a/src/api/resolvers/book/index.ts b/src/api/resolvers/book/index.ts index 5b4d625b..66dbf777 100644 --- a/src/api/resolvers/book/index.ts +++ b/src/api/resolvers/book/index.ts @@ -6,7 +6,9 @@ import { Arg, Resolver, Query, + UseMiddleware, } from 'type-graphql'; +import { isAuthenticated } from '@api/middleware/resolver/isAuthenticated'; @ObjectType() class File { @@ -29,35 +31,45 @@ class Markdown { @Resolver() export default class BookResolver { @Query(() => File) + @UseMiddleware(isAuthenticated) async book( @Arg('path') path: string, @Arg('fileName') fileName: string - ) { - const data = await s3 + ): Promise { + const { ContentType, Body } = await s3 .getObject({ Bucket: bucket, Key: path, }) .promise(); + if (!ContentType || !Body) { + throw new Error("Book couldn't get downloaded."); + } + return { fileName, - contentType: data.ContentType, - body: data?.Body?.toString('base64'), + contentType: ContentType, + body: Body.toString('base64'), }; } @Query(() => Markdown) - async onlineChapter(@Arg('path') path: string) { - const data = await s3 + @UseMiddleware(isAuthenticated) + async onlineChapter(@Arg('path') path: string): Promise { + const { Body } = await s3 .getObject({ Bucket: bucket, Key: path, }) .promise(); + if (!Body) { + throw new Error("Chapter couldn't get downloaded."); + } + return { - body: data?.Body?.toString('base64'), + body: Body.toString('base64'), }; } } diff --git a/src/api/resolvers/community/index.ts b/src/api/resolvers/community/index.ts index 80925efc..91e31521 100644 --- a/src/api/resolvers/community/index.ts +++ b/src/api/resolvers/community/index.ts @@ -1,6 +1,7 @@ -import { Arg, Resolver, Mutation } from 'type-graphql'; +import { Arg, Resolver, Mutation, UseMiddleware } from 'type-graphql'; import { inviteToSlack } from '@services/slack'; +import { isAuthenticated } from '@api/middleware/resolver/isAuthenticated'; // https://api.slack.com/methods/admin.users.invite const SLACK_ERRORS: { [key: string]: string } = { @@ -61,19 +62,20 @@ const SLACK_ERRORS: { [key: string]: string } = { @Resolver() export default class CommunityResolver { @Mutation(() => Boolean) - async communityJoin(@Arg('email') email: string) { + @UseMiddleware(isAuthenticated) + async communityJoin(@Arg('email') email: string): Promise { try { const result = await inviteToSlack(email); if (!result) { - return new Error('Something went wrong.'); + throw new Error('Something went wrong.'); } if (!result.data.ok) { - return new Error(SLACK_ERRORS[result.data.error]); + throw new Error(SLACK_ERRORS[result.data.error]); } } catch (error) { - return new Error(error); + throw new Error(error); } return true; diff --git a/src/api/resolvers/course/index.ts b/src/api/resolvers/course/index.ts index 2d4e6a09..c53a294b 100644 --- a/src/api/resolvers/course/index.ts +++ b/src/api/resolvers/course/index.ts @@ -6,6 +6,7 @@ import { Resolver, Query, Mutation, + UseMiddleware, } from 'type-graphql'; import { StorefrontCourse } from '@api/resolvers/storefront'; @@ -14,6 +15,9 @@ import { createCourse } from '@services/firebase/course'; import { mergeCourses } from '@services/course'; import { COURSE } from '@data/course-keys-types'; import { BUNDLE } from '@data/bundle-keys-types'; +import { isAuthenticated } from '@api/middleware/resolver/isAuthenticated'; +import { isFreeCourse } from '@api/middleware/resolver/isFreeCourse'; +import { isAdmin } from '@api/middleware/resolver/isAdmin'; @ObjectType() class CurriculumItem { @@ -229,17 +233,14 @@ export default class CourseResolver { })); } - @Query(() => UnlockedCourse, { nullable: true }) + @Query(() => UnlockedCourse) + @UseMiddleware(isAuthenticated) async unlockedCourse( @Arg('courseId') courseId: string, @Ctx() ctx: ResolverContext - ): Promise { - if (!ctx.me) { - return null; - } - + ): Promise { const courses = await ctx.courseConnector.getCoursesByUserIdAndCourseId( - ctx.me.uid, + ctx.me!.uid, courseId as COURSE ); @@ -253,6 +254,7 @@ export default class CourseResolver { } @Mutation(() => Boolean) + @UseMiddleware(isAuthenticated, isFreeCourse) async createFreeCourse( @Arg('courseId') courseId: string, @Arg('bundleId') bundleId: string, @@ -287,6 +289,7 @@ export default class CourseResolver { } @Mutation(() => Boolean) + @UseMiddleware(isAuthenticated, isAdmin) async createAdminCourse( @Arg('courseId') courseId: string, @Arg('bundleId') bundleId: string, diff --git a/src/api/resolvers/migration/index.ts b/src/api/resolvers/migration/index.ts index 5d57bfcf..c1e1d4c1 100644 --- a/src/api/resolvers/migration/index.ts +++ b/src/api/resolvers/migration/index.ts @@ -1,9 +1,14 @@ -import { Arg, Resolver, Mutation } from 'type-graphql'; +import { Arg, Resolver, Mutation, UseMiddleware } from 'type-graphql'; +import { isAuthenticated } from '@api/middleware/resolver/isAuthenticated'; +import { isAdmin } from '@api/middleware/resolver/isAdmin'; @Resolver() export default class MigrationResolver { @Mutation(() => Boolean) - async migrate(@Arg('migrationType') migrationType: string) { + @UseMiddleware(isAuthenticated, isAdmin) + async migrate( + @Arg('migrationType') migrationType: string + ): Promise { switch (migrationType) { case 'FOO': return true; diff --git a/src/api/resolvers/partner/index.ts b/src/api/resolvers/partner/index.ts index eb4f7b41..e6902dd7 100644 --- a/src/api/resolvers/partner/index.ts +++ b/src/api/resolvers/partner/index.ts @@ -6,10 +6,14 @@ import { Resolver, Query, Mutation, + UseMiddleware, } from 'type-graphql'; import { ResolverContext } from '@typeDefs/resolver'; import { hasPartnerRole } from '@validation/partner'; +import { isAuthenticated } from '@api/middleware/resolver/isAuthenticated'; +import { isAdmin } from '@api/middleware/resolver/isAdmin'; +import { isPartner } from '@api/middleware/resolver/isPartner'; @ObjectType() export class VisitorByDay { @@ -70,11 +74,12 @@ export class PartnerPayment { @Resolver() export default class PartnerResolver { @Query(() => [VisitorByDay]) + @UseMiddleware(isAuthenticated, isPartner) async partnerVisitors( @Arg('from') from: Date, @Arg('to') to: Date, @Ctx() ctx: ResolverContext - ) { + ): Promise { try { return await ctx.partnerConnector.getVisitorsBetweenAggregatedByDate( from, @@ -86,21 +91,18 @@ export default class PartnerResolver { } @Query(() => PartnerSaleConnection) + @UseMiddleware(isAuthenticated, isPartner) async partnerSales( @Arg('offset') offset: number, @Arg('limit') limit: number, @Ctx() ctx: ResolverContext - ) { - if (!ctx.me) { - return []; - } - + ): Promise { try { const { edges, total, } = await ctx.partnerConnector.getSalesByPartner( - ctx.me.uid, + ctx.me!.uid, offset, limit ); @@ -120,19 +122,18 @@ export default class PartnerResolver { }, }; } catch (error) { - return []; + throw new Error(error); } } @Query(() => [PartnerPayment]) - async partnerPayments(@Ctx() ctx: ResolverContext) { - if (!ctx.me) { - return []; - } - + @UseMiddleware(isAuthenticated, isPartner) + async partnerPayments( + @Ctx() ctx: ResolverContext + ): Promise { try { return await ctx.partnerConnector.getPaymentsByPartner( - ctx.me.uid + ctx.me!.uid ); } catch (error) { return []; @@ -140,10 +141,11 @@ export default class PartnerResolver { } @Mutation(() => Boolean) + @UseMiddleware(isAuthenticated, isAdmin) async promoteToPartner( @Arg('uid') uid: string, @Ctx() ctx: ResolverContext - ) { + ): Promise { try { await ctx.adminConnector.setCustomClaims(uid, { partner: true, @@ -159,7 +161,7 @@ export default class PartnerResolver { async partnerTrackVisitor( @Arg('partnerId') partnerId: string, @Ctx() ctx: ResolverContext - ) { + ): Promise { try { const partner = await ctx.adminConnector.getUser(partnerId); diff --git a/src/api/resolvers/storefront/index.ts b/src/api/resolvers/storefront/index.ts index c1fceb4b..fdc04272 100644 --- a/src/api/resolvers/storefront/index.ts +++ b/src/api/resolvers/storefront/index.ts @@ -49,7 +49,7 @@ export class StorefrontCourse { canUpgrade: boolean; @Field() - bundle: StorefrontBundle; + bundle?: StorefrontBundle; } @Resolver() @@ -58,7 +58,7 @@ export default class StorefrontResolver { async storefrontCourse( @Arg('courseId') courseId: string, @Arg('bundleId') bundleId: string - ) { + ): Promise { const course = storefront[courseId as COURSE]; const bundle = course.bundles[bundleId as BUNDLE]; @@ -74,7 +74,7 @@ export default class StorefrontResolver { } @Query(() => [StorefrontCourse]) - async storefrontCourses() { + async storefrontCourses(): Promise { return Object.values(storefront).map(storefrontCourse => ({ courseId: storefrontCourse.courseId, header: storefrontCourse.header, @@ -85,7 +85,9 @@ export default class StorefrontResolver { } @Query(() => [StorefrontBundle]) - async storefrontBundles(@Arg('courseId') courseId: string) { + async storefrontBundles( + @Arg('courseId') courseId: string + ): Promise { const course = storefront[courseId as COURSE]; return sortBy( diff --git a/src/api/resolvers/stripe/index.ts b/src/api/resolvers/stripe/index.ts index b6978b02..beb5f3d1 100644 --- a/src/api/resolvers/stripe/index.ts +++ b/src/api/resolvers/stripe/index.ts @@ -5,6 +5,7 @@ import { Ctx, Resolver, Mutation, + UseMiddleware, } from 'type-graphql'; import { COURSE } from '@data/course-keys-types'; @@ -21,6 +22,7 @@ import { priceWithDiscount } from '@services/discount'; import stripe from '@services/stripe'; import storefront from '@data/course-storefront'; +import { isAuthenticated } from '@api/middleware/resolver/isAuthenticated'; // https://stripe.com/docs/payments/checkout/one-time#create-one-time-payments @Resolver() diff --git a/src/api/resolvers/upgrade/index.ts b/src/api/resolvers/upgrade/index.ts index 5c4e51d8..80f23375 100644 --- a/src/api/resolvers/upgrade/index.ts +++ b/src/api/resolvers/upgrade/index.ts @@ -1,23 +1,27 @@ -import { Arg, Ctx, Resolver, Query } from 'type-graphql'; +import { + Arg, + Ctx, + Resolver, + Query, + UseMiddleware, +} from 'type-graphql'; import { StorefrontCourse } from '@api/resolvers/storefront'; import { ResolverContext } from '@typeDefs/resolver'; import { getUpgradeableCourses } from '@services/course'; import { COURSE } from '@data/course-keys-types'; +import { isAuthenticated } from '@api/middleware/resolver/isAuthenticated'; @Resolver() export default class UpgradeResolver { @Query(() => [StorefrontCourse]) + @UseMiddleware(isAuthenticated) async upgradeableCourses( @Arg('courseId') courseId: string, @Ctx() ctx: ResolverContext ) { - if (!ctx.me) { - return []; - } - const courses = await ctx.courseConnector.getCoursesByUserIdAndCourseId( - ctx.me.uid, + ctx.me!.uid, courseId as COURSE ); diff --git a/src/generated/client.tsx b/src/generated/client.tsx index 6347986e..a88b08e9 100644 --- a/src/generated/client.tsx +++ b/src/generated/client.tsx @@ -129,7 +129,7 @@ export type Mutation = { stripeCreateOrder: StripeId; createFreeCourse: Scalars['Boolean']; createAdminCourse: Scalars['Boolean']; - couponCreate?: Maybe; + couponCreate: Scalars['Boolean']; promoteToPartner: Scalars['Boolean']; partnerTrackVisitor: Scalars['Boolean']; communityJoin: Scalars['Boolean']; @@ -284,7 +284,7 @@ export type Query = { storefrontCourses: Array; storefrontBundles: Array; unlockedCourses: Array; - unlockedCourse?: Maybe; + unlockedCourse: UnlockedCourse; book: File; onlineChapter: Markdown; upgradeableCourses: Array; @@ -391,8 +391,8 @@ export type UnlockedCourse = { export type User = { __typename?: 'User'; - email: Scalars['String']; uid: Scalars['String']; + email: Scalars['String']; username: Scalars['String']; roles: Array; }; @@ -485,7 +485,7 @@ export type GetCourseQueryVariables = { export type GetCourseQuery = ( { __typename?: 'Query' } - & { unlockedCourse?: Maybe<( + & { unlockedCourse: ( { __typename?: 'UnlockedCourse' } & Pick & { introduction?: Maybe<( @@ -544,7 +544,7 @@ export type GetCourseQuery = ( )> } )> } )> } - )> } + ) } ); export type CreateFreeCourseMutationVariables = { From 1f5601da99da0c6595f778c488724b184f110e88 Mon Sep 17 00:00:00 2001 From: Robin Wieruch Date: Thu, 16 Apr 2020 15:45:14 +0200 Subject: [PATCH 11/13] indicators --- src/screens/Account/index.tsx | 2 +- src/screens/EmailChange/EmailChangeForm/index.tsx | 3 +++ .../PasswordChange/PasswordChangeForm/index.tsx | 3 +++ .../PasswordForgot/PasswordForgotForm/index.tsx | 10 ++++++++-- 4 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/screens/Account/index.tsx b/src/screens/Account/index.tsx index a75b1f1a..6b2d45c5 100644 --- a/src/screens/Account/index.tsx +++ b/src/screens/Account/index.tsx @@ -66,7 +66,7 @@ const AccountPage: NextAuthPage = ({ data }) => { - +
  • Partner Program diff --git a/src/screens/EmailChange/EmailChangeForm/index.tsx b/src/screens/EmailChange/EmailChangeForm/index.tsx index 8a2445b5..aea308ec 100644 --- a/src/screens/EmailChange/EmailChangeForm/index.tsx +++ b/src/screens/EmailChange/EmailChangeForm/index.tsx @@ -18,6 +18,9 @@ const EmailChangeForm = ({ form }: EmailChangeFormProps) => { const { successMessage } = useIndicators({ key: 'email-change', error, + success: { + message: 'Success! You can use your new email now.', + }, }); const [confirmEmailDirty, setConfirmEmailDirty] = React.useState( diff --git a/src/screens/PasswordChange/PasswordChangeForm/index.tsx b/src/screens/PasswordChange/PasswordChangeForm/index.tsx index 1de986cc..0d96045c 100644 --- a/src/screens/PasswordChange/PasswordChangeForm/index.tsx +++ b/src/screens/PasswordChange/PasswordChangeForm/index.tsx @@ -22,6 +22,9 @@ const PasswordChangeForm = ({ form }: PasswordChangeFormProps) => { const { successMessage } = useIndicators({ key: 'password-change', error, + success: { + message: 'Success! You can use your new password now.', + }, }); const [ diff --git a/src/screens/PasswordForgot/PasswordForgotForm/index.tsx b/src/screens/PasswordForgot/PasswordForgotForm/index.tsx index 52322e25..acede6a8 100644 --- a/src/screens/PasswordForgot/PasswordForgotForm/index.tsx +++ b/src/screens/PasswordForgot/PasswordForgotForm/index.tsx @@ -5,7 +5,7 @@ import { FormComponentProps } from 'antd/lib/form'; import { usePasswordForgotMutation } from '@generated/client'; import FormItem from '@components/Form/Item'; import FormStretchedButton from '@components/Form/StretchedButton'; -import useErrorIndicator from '@hooks/useErrorIndicator'; +import useIndicators from '@hooks/useIndicators'; interface PasswordForgotFormProps extends FormComponentProps {} @@ -15,7 +15,11 @@ const PasswordForgotForm = ({ form }: PasswordForgotFormProps) => { { loading, error }, ] = usePasswordForgotMutation(); - useErrorIndicator({ error }); + const { successMessage } = useIndicators({ + key: 'password-forgot', + error, + success: { message: 'Success! Check your email inbox.' }, + }); const handleSubmit = (event: React.FormEvent) => { form.validateFields(async (error, values) => { @@ -28,6 +32,8 @@ const PasswordForgotForm = ({ form }: PasswordForgotFormProps) => { }, }); + successMessage(); + form.resetFields(); } catch (error) {} }); From 6d221c21b376ea0b628860ec5fea2a22753e8f27 Mon Sep 17 00:00:00 2001 From: Robin Wieruch Date: Sun, 19 Apr 2020 12:00:26 +0200 Subject: [PATCH 12/13] rm unused file --- src/api/schema/index.ts | 20 -------------------- 1 file changed, 20 deletions(-) delete mode 100644 src/api/schema/index.ts diff --git a/src/api/schema/index.ts b/src/api/schema/index.ts deleted file mode 100644 index c799eb48..00000000 --- a/src/api/schema/index.ts +++ /dev/null @@ -1,20 +0,0 @@ -// import { mergeSchemas } from 'graphql-tools'; -// import { applyMiddleware } from 'graphql-middleware'; - -// import { ResolverContext } from '@typeDefs/resolver'; - -// import { Resolvers } from '@generated/server'; - -// import authorization from '@api/authorization'; -// import typeDefs from '@api/typeDefs'; -// import resolvers from '@api/resolvers'; - -// const schema = mergeSchemas({ -// schemas: typeDefs, -// resolvers: resolvers as Resolvers, -// }); - -// export default applyMiddleware( -// schema, -// authorization, -// ); From cf99c707b16c95cc290e1ac05adc955912bfbef0 Mon Sep 17 00:00:00 2001 From: Robin Wieruch Date: Sun, 19 Apr 2020 13:24:48 +0200 Subject: [PATCH 13/13] fix import --- src/data/migration.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/data/migration.ts b/src/data/migration.ts index 98288fb3..9a5f7e42 100644 --- a/src/data/migration.ts +++ b/src/data/migration.ts @@ -1,6 +1,6 @@ import invert from 'lodash.invert'; -import { COURSE } from './course-keys'; +import { COURSE } from './course-keys-types'; import BUNDLE_LEGACY from './bundle-legacy'; const applyMigration = (