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/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: 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", diff --git a/pages/api/graphql.ts b/pages/api/graphql.ts index eed1882e..9c92af4c 100644 --- a/pages/api/graphql.ts +++ b/pages/api/graphql.ts @@ -1,6 +1,11 @@ import { ApolloServer } from 'apollo-server-micro'; +import { buildSchema } from 'type-graphql'; +import { applyMiddleware } from 'graphql-middleware'; + import cors from 'micro-cors'; +import 'reflect-metadata'; + import getConnection from '@models/index'; import { AdminConnector } from '@connectors/admin'; import { PartnerConnector } from '@connectors/partner'; @@ -8,8 +13,11 @@ 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 getMe from '@api/middleware/getMe'; + +import resolvers from '@api/resolvers'; +import meMiddleware from '@api/middleware/global/me'; +import sentryMiddleware from '@api/middleware/global/sentry'; + import firebaseAdmin from '@services/firebase/admin'; if (process.env.FIREBASE_ADMIN_UID) { @@ -41,11 +49,15 @@ 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 => { - const me = await getMe(req, res); + schema: applyMiddleware(schema, sentryMiddleware, meMiddleware), + context: async ({ req, res }): Promise => { const adminConnector = new AdminConnector(); const partnerConnector = new PartnerConnector(connection!); const courseConnector = new CourseConnector(connection!); @@ -54,7 +66,6 @@ export default async (req: ServerRequest, res: ServerResponse) => { return { req, res, - me, adminConnector, courseConnector, partnerConnector, @@ -67,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/authorization/index.ts b/src/api/authorization/index.ts deleted file mode 100644 index 2fd3ac1f..00000000 --- a/src/api/authorization/index.ts +++ /dev/null @@ -1,34 +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, - 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 7b57e010..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'; -import { BUNDLE } from '@data/bundle-keys'; - -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/getMe.ts b/src/api/middleware/global/me.ts similarity index 55% rename from src/api/middleware/getMe.ts rename to src/api/middleware/global/me.ts index 97e093cd..bb73b495 100644 --- a/src/api/middleware/getMe.ts +++ b/src/api/middleware/global/me.ts @@ -2,19 +2,25 @@ import { AuthenticationError } from 'apollo-server-micro'; import firebaseAdmin from '@services/firebase/admin'; -import { ServerResponse, ServerRequest } from '@typeDefs/server'; +import { ResolverContext } from '@typeDefs/resolver'; 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; + return await resolve(root, args, context, info); } const CHECK_REVOKED = true; - return await firebaseAdmin + const me = await firebaseAdmin .auth() .verifySessionCookie(session, CHECK_REVOKED) .then(async claims => { @@ -23,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/schema/index.ts b/src/api/middleware/global/sentry.ts similarity index 57% rename from src/api/schema/index.ts rename to src/api/middleware/global/sentry.ts index 0c9eb83a..e42874d2 100644 --- a/src/api/schema/index.ts +++ b/src/api/middleware/global/sentry.ts @@ -1,5 +1,3 @@ -import { mergeSchemas } from 'graphql-tools'; -import { applyMiddleware } from 'graphql-middleware'; import { sentry } from 'graphql-middleware-sentry'; import * as Sentry from '@sentry/node'; @@ -9,7 +7,7 @@ Sentry.init({ dsn: process.env.SENTRY_DSN, }); -const sentryMiddleware = sentry({ +export default sentry({ sentryInstance: Sentry, config: { environment: process.env.NODE_ENV, @@ -27,20 +25,3 @@ const sentryMiddleware = sentry({ 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/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 new file mode 100644 index 00000000..86026e99 --- /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) { + throw new ForbiddenError('Not authenticated as user.'); + } + + return next(); +}; 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 57800594..66dbf777 100644 --- a/src/api/resolvers/book/index.ts +++ b/src/api/resolvers/book/index.ts @@ -1,37 +1,75 @@ -import { QueryResolvers } from '@generated/server'; import s3, { bucket } from '@services/aws/s3'; -interface Resolvers { - Query: QueryResolvers; +import { + ObjectType, + Field, + Arg, + Resolver, + Query, + UseMiddleware, +} from 'type-graphql'; +import { isAuthenticated } from '@api/middleware/resolver/isAuthenticated'; + +@ObjectType() +class File { + @Field() + fileName: string; + + @Field() + contentType: string; + + @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'), - }; - }, - }, -}; +@ObjectType() +class Markdown { + @Field() + body: string; +} + +@Resolver() +export default class BookResolver { + @Query(() => File) + @UseMiddleware(isAuthenticated) + async book( + @Arg('path') path: string, + @Arg('fileName') fileName: string + ): 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: ContentType, + body: Body.toString('base64'), + }; + } + + @Query(() => Markdown) + @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: Body.toString('base64'), + }; + } +} diff --git a/src/api/resolvers/community/index.ts b/src/api/resolvers/community/index.ts index 54454906..91e31521 100644 --- a/src/api/resolvers/community/index.ts +++ b/src/api/resolvers/community/index.ts @@ -1,9 +1,7 @@ -import { MutationResolvers } from '@generated/server'; -import { inviteToSlack } from '@services/slack'; +import { Arg, Resolver, Mutation, UseMiddleware } from 'type-graphql'; -interface Resolvers { - Mutation: MutationResolvers; -} +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,24 +59,25 @@ 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) + @UseMiddleware(isAuthenticated) + async communityJoin(@Arg('email') email: string): Promise { + try { + const result = await inviteToSlack(email); - if (!result) { - return new Error('Something went wrong.'); - } + if (!result) { + throw 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) { + throw new Error(SLACK_ERRORS[result.data.error]); } + } catch (error) { + throw 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..12528f2c 100644 --- a/src/api/resolvers/coupon/index.ts +++ b/src/api/resolvers/coupon/index.ts @@ -1,50 +1,78 @@ -import { QueryResolvers, MutationResolvers } from '@generated/server'; +import { + ObjectType, + Field, + Arg, + Ctx, + Resolver, + Query, + Mutation, + UseMiddleware, +} 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'; +import { isAuthenticated } from '@api/middleware/resolver/isAuthenticated'; +import { isAdmin } from '@api/middleware/resolver/isAdmin'; +@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]; +@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 (!me) { - return bundle.price; - } + const price = await priceWithDiscount( + ctx.couponConnector, + ctx.courseConnector + )( + courseId as COURSE, + bundleId as BUNDLE, + bundle.price, + coupon, + ctx.me!.uid + ); - const price = await priceWithDiscount( - couponConnector, - courseConnector - )(courseId, bundleId, bundle.price, coupon, me.uid); + return { + price, + isDiscount: price !== bundle.price, + }; + } - 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); - } + @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, + discount, + count + ); + } catch (error) { + throw new Error(error); + } - return true; - }, - }, -}; + 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..c53a294b 100644 --- a/src/api/resolvers/course/index.ts +++ b/src/api/resolvers/course/index.ts @@ -1,116 +1,322 @@ -import { QueryResolvers, MutationResolvers } from '@generated/server'; +import { + ObjectType, + Field, + Arg, + Ctx, + Resolver, + Query, + Mutation, + UseMiddleware, +} 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'; +import { isAuthenticated } from '@api/middleware/resolver/isAuthenticated'; +import { isFreeCourse } from '@api/middleware/resolver/isFreeCourse'; +import { isAdmin } from '@api/middleware/resolver/isAdmin'; + +@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) + @UseMiddleware(isAuthenticated) + async unlockedCourse( + @Arg('courseId') courseId: string, + @Ctx() ctx: ResolverContext + ): Promise { + 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) + @UseMiddleware(isAuthenticated, isFreeCourse) + async createFreeCourse( + @Arg('courseId') courseId: string, + @Arg('bundleId') bundleId: string, + @Ctx() ctx: ResolverContext + ): Promise { + 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) + @UseMiddleware(isAuthenticated, isAdmin) + async createAdminCourse( + @Arg('courseId') courseId: string, + @Arg('bundleId') bundleId: string, + @Arg('uid') uid: string, + @Ctx() ctx: ResolverContext + ): Promise { + 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..ca63bee4 100644 --- a/src/api/resolvers/index.ts +++ b/src/api/resolvers/index.ts @@ -1,33 +1,29 @@ -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'; +import { NonEmptyArray } from 'type-graphql'; -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, +] as NonEmptyArray; diff --git a/src/api/resolvers/migration/index.ts b/src/api/resolvers/migration/index.ts index 9b4283c3..c1e1d4c1 100644 --- a/src/api/resolvers/migration/index.ts +++ b/src/api/resolvers/migration/index.ts @@ -1,20 +1,21 @@ -import { MutationResolvers } from '@generated/server'; +import { Arg, Resolver, Mutation, UseMiddleware } from 'type-graphql'; +import { isAuthenticated } from '@api/middleware/resolver/isAuthenticated'; +import { isAdmin } from '@api/middleware/resolver/isAdmin'; -interface Resolvers { - Mutation: MutationResolvers; +@Resolver() +export default class MigrationResolver { + @Mutation(() => Boolean) + @UseMiddleware(isAuthenticated, isAdmin) + async migrate( + @Arg('migrationType') migrationType: string + ): Promise { + 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..e6902dd7 100644 --- a/src/api/resolvers/partner/index.ts +++ b/src/api/resolvers/partner/index.ts @@ -1,110 +1,183 @@ -import { QueryResolvers, MutationResolvers } from '@generated/server'; +import { + ObjectType, + Field, + Arg, + Ctx, + 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 { + @Field() + date: Date; -interface Resolvers { - Query: QueryResolvers; - Mutation: MutationResolvers; + @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; +} + +@ObjectType() +export class PartnerPayment { + @Field() + createdAt: Date; + + @Field() + royalty: number; +} +@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, + to + ); + } catch (error) { + return []; + } + } - try { - const { - edges, + @Query(() => PartnerSaleConnection) + @UseMiddleware(isAuthenticated, isPartner) + async partnerSales( + @Arg('offset') offset: number, + @Arg('limit') limit: number, + @Ctx() ctx: ResolverContext + ): Promise { + 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) { + throw new Error(error); + } + } - 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]) + @UseMiddleware(isAuthenticated, isPartner) + async partnerPayments( + @Ctx() ctx: ResolverContext + ): Promise { + try { + return await ctx.partnerConnector.getPaymentsByPartner( + ctx.me!.uid + ); + } catch (error) { + return []; + } + } - return true; - }, - partnerTrackVisitor: async ( - _, - { partnerId }, - { partnerConnector, adminConnector } - ) => { - try { - const partner = await adminConnector.getUser(partnerId); - - if (!hasPartnerRole(partner)) { - return false; - } - } catch (error) { - return false; - } + @Mutation(() => Boolean) + @UseMiddleware(isAuthenticated, isAdmin) + async promoteToPartner( + @Arg('uid') uid: string, + @Ctx() ctx: ResolverContext + ): Promise { + try { + await ctx.adminConnector.setCustomClaims(uid, { + partner: true, + }); + } catch (error) { + throw new Error(error); + } + + return true; + } - try { - await partnerConnector.createVisitor(partnerId); - } catch (error) { + @Mutation(() => Boolean) + async partnerTrackVisitor( + @Arg('partnerId') partnerId: string, + @Ctx() ctx: ResolverContext + ): Promise { + try { + const partner = await ctx.adminConnector.getUser(partnerId); + + if (!hasPartnerRole(partner)) { return false; } + } catch (error) { + return false; + } + + try { + await ctx.partnerConnector.createVisitor(partnerId); + } catch (error) { + return false; + } - return true; - }, - }, -}; + 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..8c727b83 100644 --- a/src/api/resolvers/session/index.ts +++ b/src/api/resolvers/session/index.ts @@ -1,118 +1,160 @@ -import { MutationResolvers } from '@generated/server'; +import { + ObjectType, + Field, + Arg, + Ctx, + Resolver, + Mutation, + UseMiddleware, +} 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'; +import { isAuthenticated } from '@api/middleware/resolver/isAuthenticated'; -interface Resolvers { - Mutation: MutationResolvers; +@ObjectType() +class SessionToken { + @Field() + token: string; } -export const resolvers: Resolvers = { - Mutation: { - signIn: async (_, { email, password }) => { - 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(); - - 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 +@Resolver() +export default class SessionResolver { + @Mutation(() => SessionToken) + async signIn( + @Arg('email') email: string, + @Arg('password') password: string + ): Promise { + let result; + + try { + result = await firebase .auth() .signInWithEmailAndPassword(email, password); + } catch (error) { + throw new Error(error); + } - const idToken = await user?.getIdToken(); - const sessionToken = await firebaseAdmin - .auth() - .createSessionCookie(idToken || '', { - expiresIn: EXPIRES_IN, - }); - - // We manage the session ourselves. - await firebase.auth().signOut(); - - try { - inviteToConvertkit(email, username); - } catch (error) { - console.log(error); - } - - try { - inviteToRevue(email, username); - } catch (error) { - console.log(error); - } - - return { sessionToken }; - }, - passwordForgot: async (_, { email }) => { - 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; - }, - emailChange: async (_, { email }, { me }) => { - try { - await firebaseAdmin.auth().updateUser(me?.uid || '', { - email, - }); - } catch (error) { - return new Error(error); - } - - return true; - }, - }, -}; + if (!result.user) { + throw new Error('No user found.'); + } + + const idToken = await result.user.getIdToken(); + + const token = await firebaseAdmin + .auth() + .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 { token }; + } + + @Mutation(() => SessionToken) + async signUp( + @Arg('username') username: string, + @Arg('email') email: string, + @Arg('password') password: string + ): Promise { + try { + await firebaseAdmin.auth().createUser({ + email, + password, + displayName: username, + }); + } catch (error) { + throw new Error(error); + } + + const { user } = await firebase + .auth() + .signInWithEmailAndPassword(email, password); + + if (!user) { + throw new Error('No user found.'); + } + + const idToken = await user.getIdToken(); + + const token = await firebaseAdmin + .auth() + .createSessionCookie(idToken, { + expiresIn: EXPIRES_IN, + }); + + // We manage the session ourselves. + await firebase.auth().signOut(); + + try { + inviteToConvertkit(email, username); + } catch (error) { + console.log(error); + } + + try { + inviteToRevue(email, username); + } catch (error) { + console.log(error); + } + + return { token }; + } + + @Mutation(() => Boolean) + async passwordForgot( + @Arg('email') email: string + ): Promise { + try { + await firebase.auth().sendPasswordResetEmail(email); + } catch (error) { + throw new Error(error); + } + + return true; + } + + @Mutation(() => Boolean) + @UseMiddleware(isAuthenticated) + async passwordChange( + @Arg('password') password: string, + @Ctx() ctx: ResolverContext + ): Promise { + try { + await firebaseAdmin.auth().updateUser(ctx.me!.uid, { + password, + }); + } catch (error) { + throw new Error(error); + } + + return true; + } + + @Mutation(() => Boolean) + @UseMiddleware(isAuthenticated) + async emailChange( + @Arg('email') email: string, + @Ctx() ctx: ResolverContext + ): Promise { + try { + await firebaseAdmin.auth().updateUser(ctx.me!.uid, { + email, + }); + } catch (error) { + throw 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..fdc04272 100644 --- a/src/api/resolvers/storefront/index.ts +++ b/src/api/resolvers/storefront/index.ts @@ -1,44 +1,98 @@ +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 + ): Promise { + 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(): Promise { + 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 + ): Promise { + 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..beb5f3d1 100644 --- a/src/api/resolvers/stripe/index.ts +++ b/src/api/resolvers/stripe/index.ts @@ -1,67 +1,97 @@ -import { MutationResolvers } from '@generated/server'; +import { + ObjectType, + Field, + Arg, + Ctx, + Resolver, + Mutation, + UseMiddleware, +} 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'; +import { isAuthenticated } from '@api/middleware/resolver/isAuthenticated'; -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..80f23375 100644 --- a/src/api/resolvers/upgrade/index.ts +++ b/src/api/resolvers/upgrade/index.ts @@ -1,27 +1,30 @@ -import { QueryResolvers } from '@generated/server'; -import { getUpgradeableCourses } from '@services/course'; - -interface Resolvers { - Query: QueryResolvers; -} +import { + Arg, + Ctx, + Resolver, + Query, + UseMiddleware, +} from 'type-graphql'; -export const resolvers: Resolvers = { - Query: { - upgradeableCourses: async ( - _, - { courseId }, - { me, courseConnector } - ) => { - if (!me) { - return []; - } +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'; - const courses = await courseConnector.getCoursesByUserIdAndCourseId( - me.uid, - courseId - ); +@Resolver() +export default class UpgradeResolver { + @Query(() => [StorefrontCourse]) + @UseMiddleware(isAuthenticated) + async upgradeableCourses( + @Arg('courseId') courseId: string, + @Ctx() ctx: ResolverContext + ) { + 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..3f0a4707 100644 --- a/src/api/resolvers/user/index.ts +++ b/src/api/resolvers/user/index.ts @@ -1,23 +1,46 @@ -import { QueryResolvers } from '@generated/server'; +import { + ObjectType, + Field, + Ctx, + Resolver, + Query, + UseMiddleware, +} from 'type-graphql'; -interface Resolvers { - Query: QueryResolvers; +import { ResolverContext } from '@typeDefs/resolver'; +import { isAuthenticated } from '@api/middleware/resolver/isAuthenticated'; + +@ObjectType() +class User { + @Field() + uid: string; + + @Field() + email: 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] - ); - - return { - email: me?.email, - uid: me?.uid, - username: me?.displayName, - roles, - }; - }, - }, -}; +@Resolver() +export default class UserResolver { + @Query(() => User) + @UseMiddleware(isAuthenticated) + async me(@Ctx() ctx: ResolverContext): Promise { + const rolesObject = ctx.me!.customClaims || {}; + + const roles = Object.keys(rolesObject).filter( + key => rolesObject[key] + ); + + return { + uid: ctx.me!.uid, + email: ctx.me!.email || '', + 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/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/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, -]; 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/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; 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 = ( diff --git a/src/generated/client.tsx b/src/generated/client.tsx index 281f519c..a88b08e9 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; + passwordForgot: Scalars['Boolean']; + passwordChange: Scalars['Boolean']; + emailChange: Scalars['Boolean']; + paypalCreateOrder: PaypalOrderId; + paypalApproveOrder: Scalars['Boolean']; stripeCreateOrder: StripeId; createFreeCourse: Scalars['Boolean']; createAdminCourse: Scalars['Boolean']; - couponCreate?: Maybe; - promoteToPartner?: Maybe; - partnerTrackVisitor?: Maybe; - communityJoin?: Maybe; + couponCreate: Scalars['Boolean']; + 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,15 +272,19 @@ 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; - unlockedCourse?: Maybe; + unlockedCourse: UnlockedCourse; book: File; onlineChapter: Markdown; upgradeableCourses: 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,38 +323,38 @@ 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 = { __typename?: 'SessionToken'; - sessionToken: Scalars['String']; + token: Scalars['String']; }; 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']; @@ -416,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; }; @@ -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,13 +479,13 @@ export type GetCoursesQuery = ( ); export type GetCourseQueryVariables = { - courseId: CourseId; + courseId: Scalars['String']; }; export type GetCourseQuery = ( { __typename?: 'Query' } - & { unlockedCourse?: Maybe<( + & { unlockedCourse: ( { __typename?: 'UnlockedCourse' } & Pick & { introduction?: Maybe<( @@ -569,12 +544,12 @@ 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 ) } ); @@ -707,7 +682,7 @@ export type SignUpMutation = ( { __typename?: 'Mutation' } & { signUp: ( { __typename?: 'SessionToken' } - & Pick + & Pick ) } ); @@ -721,7 +696,7 @@ export type SignInMutation = ( { __typename?: 'Mutation' } & { signIn: ( { __typename?: 'SessionToken' } - & Pick + & 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 } @@ -1460,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 } } `; @@ -1494,7 +1469,7 @@ export type SignUpMutationOptions = ApolloReactCommon.BaseMutationOptions; 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..7c07ecdf 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'; @@ -14,9 +13,7 @@ export default async function() { if (connection.isConnected) { await connection.close(); } - } catch (error) { - console.log(error); - } + } catch (error) {} try { connection = await createConnection({ @@ -39,9 +36,7 @@ export default async function() { }, }), }); - } catch (error) { - console.log(error); - } + } catch (error) {} return connection; } 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/session.ts b/src/queries/session.ts index 10db2fc7..96babfbd 100644 --- a/src/queries/session.ts +++ b/src/queries/session.ts @@ -7,7 +7,7 @@ export const SIGN_UP = gql` $password: String! ) { signUp(username: $username, email: $email, password: $password) { - sessionToken + token } } `; @@ -15,7 +15,7 @@ export const SIGN_UP = gql` export const SIGN_IN = gql` mutation SignIn($email: String!, $password: String!) { signIn(email: $email, password: $password) { - sessionToken + token } } `; 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/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 5105683e..0d96045c 100644 --- a/src/screens/PasswordChange/PasswordChangeForm/index.tsx +++ b/src/screens/PasswordChange/PasswordChangeForm/index.tsx @@ -23,7 +23,7 @@ const PasswordChangeForm = ({ form }: PasswordChangeFormProps) => { key: 'password-change', error, success: { - message: 'Success! Check your email inbox.', + message: 'Success! You can use your new password now.', }, }); 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) {} }); 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' } } }; }, }, ]; 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; -}; 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]; 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,