├── .dockerignore ├── .prettierrc ├── scripts ├── docker_setup_init.sh └── web-docker-entrypoint.sh ├── src ├── server │ ├── config │ │ └── constants.ts │ ├── app │ │ ├── things │ │ │ ├── dto │ │ │ │ └── create-thing.dto.ts │ │ │ ├── things.module.ts │ │ │ ├── things.resolver.ts │ │ │ ├── thing.entity.ts │ │ │ └── things.service.ts │ │ ├── app.service.ts │ │ ├── auth │ │ │ ├── jwt │ │ │ │ ├── jwt-auth.guard.ts │ │ │ │ ├── jwt-auth.service.ts │ │ │ │ ├── jwt-auth.module.ts │ │ │ │ └── jwt-auth.strategy.ts │ │ │ ├── google │ │ │ │ ├── google-oauth.guard.ts │ │ │ │ ├── google-oauth.module.ts │ │ │ │ ├── google-oauth.controller.ts │ │ │ │ └── google-oauth.strategy.ts │ │ │ ├── cognito │ │ │ │ ├── cognito-oauth.guard.ts │ │ │ │ ├── cognito-oauth.module.ts │ │ │ │ ├── cognito-oauth.controller.ts │ │ │ │ └── cognito-oauth.strategy.ts │ │ │ ├── auth.controller.ts │ │ │ ├── graphql │ │ │ │ ├── gql-auth.decorator.ts │ │ │ │ └── gql-auth.guard.ts │ │ │ └── auth.module.ts │ │ ├── users │ │ │ ├── dto │ │ │ │ └── create-user.dto.ts │ │ │ ├── users.module.ts │ │ │ ├── users.service.ts │ │ │ ├── users.resolver.ts │ │ │ └── user.entity.ts │ │ ├── orders │ │ │ ├── dto │ │ │ │ └── create-order.dto.ts │ │ │ ├── orders.module.ts │ │ │ ├── order.entity.ts │ │ │ ├── orders.resolver.ts │ │ │ ├── orders.service.ts │ │ │ └── orders.resolver.spec.ts │ │ ├── app.controller.ts │ │ ├── app.controller.spec.ts │ │ └── app.module.ts │ ├── common │ │ └── types │ │ │ ├── express.d.ts │ │ │ └── user.ts │ ├── server.module.ts │ ├── view │ │ ├── view.module.ts │ │ ├── view.controller.ts │ │ └── view.service.ts │ ├── main.ts │ ├── migration │ │ ├── 1643046828766-MakeUserNameOptional.ts │ │ ├── 1619278600863-CreateUsers.ts │ │ └── 1620037951321-AddThingsAndOrders.ts │ ├── console.ts │ └── console │ │ └── seed.service.ts ├── client │ ├── app │ │ ├── types │ │ │ ├── utils.d.ts │ │ │ └── zeus │ │ │ │ ├── const.ts │ │ │ │ └── index.ts │ │ └── apollo-client.ts │ ├── next.config.js │ ├── next-env.d.ts │ ├── pages │ │ ├── profile.tsx │ │ ├── home.tsx │ │ ├── _app.tsx │ │ └── orders.tsx │ ├── tsconfig.json │ └── .eslintrc.js └── schema.gql ├── nest-cli.json ├── .env.test ├── tsconfig.build.json ├── Dockerfile ├── .vscode ├── settings.json └── extensions.json ├── cypress ├── e2e │ ├── index.cy.ts │ └── home.cy.ts ├── tsconfig.json ├── .eslintrc.js └── support │ ├── e2e.ts │ └── commands.ts ├── .github ├── dependabot.yml └── workflows │ ├── dependabot.yml │ └── ci.yml ├── cypress.config.ts ├── heroku.yml ├── cypress.docker.config.ts ├── Dockerfile.prod ├── test ├── factories │ ├── index.ts │ ├── thing.ts │ ├── order.ts │ └── user.ts ├── jest-request.json ├── utils.ts └── app.request-spec.ts ├── CODE_OF_CONDUCT.md ├── .env.example ├── jest.config.ts ├── ormconfig.ts ├── tsconfig.json ├── docker-compose.yml ├── .eslintrc.js ├── package.json ├── README.md └── .gitignore /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .next 3 | dist 4 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } -------------------------------------------------------------------------------- /scripts/docker_setup_init.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | cp .env.example .env 4 | -------------------------------------------------------------------------------- /src/server/config/constants.ts: -------------------------------------------------------------------------------- 1 | export const SESSION_COOKIE_KEY = 'session'; 2 | -------------------------------------------------------------------------------- /nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "collection": "@nestjs/schematics", 3 | "sourceRoot": "src/server" 4 | } 5 | -------------------------------------------------------------------------------- /.env.test: -------------------------------------------------------------------------------- 1 | DATABASE_URL=postgres://postgres:@db:5432/test 2 | JWT_SECRET=secret 3 | JWT_EXPIRES_IN=10d 4 | -------------------------------------------------------------------------------- /src/client/app/types/utils.d.ts: -------------------------------------------------------------------------------- 1 | type ExtractPromiseType = T extends PromiseLike ? U : T; 2 | -------------------------------------------------------------------------------- /src/server/app/things/dto/create-thing.dto.ts: -------------------------------------------------------------------------------- 1 | export class CreateThingDto { 2 | name: string; 3 | } 4 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["test", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:18.10.0-alpine 2 | 3 | WORKDIR /app 4 | 5 | ENTRYPOINT ["scripts/web-docker-entrypoint.sh"] 6 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": false, 3 | "editor.codeActionsOnSave": { 4 | "source.fixAll.eslint": "explicit" 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /cypress/e2e/index.cy.ts: -------------------------------------------------------------------------------- 1 | describe('/', () => { 2 | it('contains welcome message', () => { 3 | cy.visit('/'); 4 | cy.contains('Hello from NestJS'); 5 | }); 6 | }); 7 | -------------------------------------------------------------------------------- /src/server/common/types/express.d.ts: -------------------------------------------------------------------------------- 1 | import { User } from './user'; 2 | 3 | declare module 'express' { 4 | export interface Request { 5 | user?: User; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "editorconfig.editorconfig", 4 | "esbenp.prettier-vscode", 5 | "dbaeumer.vscode-eslint" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "npm" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | open-pull-requests-limit: 5 8 | -------------------------------------------------------------------------------- /cypress/e2e/home.cy.ts: -------------------------------------------------------------------------------- 1 | describe('/home', () => { 2 | it('contains welcome message', () => { 3 | cy.visit('/home'); 4 | cy.contains('Hello from NextJS'); 5 | }); 6 | }); 7 | -------------------------------------------------------------------------------- /src/client/next.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | distDir: '../../.next', 3 | eslint: { 4 | dirs: ['src/client'], // https://github.com/thisismydesign/nestjs-starter/issues/82 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /cypress.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'cypress'; 2 | 3 | export default defineConfig({ 4 | e2e: { 5 | baseUrl: 'http://localhost:3000/', 6 | videoUploadOnPasses: false, 7 | }, 8 | }); 9 | -------------------------------------------------------------------------------- /heroku.yml: -------------------------------------------------------------------------------- 1 | build: 2 | docker: 3 | web: Dockerfile.prod 4 | release: 5 | image: web 6 | command: 7 | - yarn typeorm migration:run && yarn console seed 8 | run: 9 | web: yarn start:prod 10 | -------------------------------------------------------------------------------- /src/server/app/app.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | @Injectable() 4 | export class AppService { 5 | getHello(): string { 6 | return 'Hello from NestJS!'; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/server/app/auth/jwt/jwt-auth.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { AuthGuard } from '@nestjs/passport'; 3 | 4 | @Injectable() 5 | export class JwtAuthGuard extends AuthGuard('jwt') {} 6 | -------------------------------------------------------------------------------- /cypress.docker.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'cypress'; 2 | 3 | export default defineConfig({ 4 | e2e: { 5 | baseUrl: 'http://host.docker.internal:3000/', 6 | videoUploadOnPasses: false, 7 | }, 8 | }); 9 | -------------------------------------------------------------------------------- /src/server/app/auth/google/google-oauth.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { AuthGuard } from '@nestjs/passport'; 3 | 4 | @Injectable() 5 | export class GoogleOauthGuard extends AuthGuard('google') {} 6 | -------------------------------------------------------------------------------- /src/server/app/auth/cognito/cognito-oauth.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { AuthGuard } from '@nestjs/passport'; 3 | 4 | @Injectable() 5 | export class CognitoOauthGuard extends AuthGuard('cognito') {} 6 | -------------------------------------------------------------------------------- /Dockerfile.prod: -------------------------------------------------------------------------------- 1 | FROM node:18.10.0-alpine 2 | 3 | WORKDIR /app 4 | 5 | COPY package.json yarn.lock ./ 6 | RUN yarn install 7 | 8 | COPY . /app 9 | COPY .env.example /app/.env 10 | 11 | RUN yarn build 12 | 13 | CMD yarn start:prod 14 | -------------------------------------------------------------------------------- /src/client/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /src/server/app/users/dto/create-user.dto.ts: -------------------------------------------------------------------------------- 1 | import { Provider } from 'src/server/common/types/user'; 2 | 3 | export class CreateUserDto { 4 | provider: Provider; 5 | providerId: string; 6 | username: string; 7 | name: string; 8 | } 9 | -------------------------------------------------------------------------------- /test/factories/index.ts: -------------------------------------------------------------------------------- 1 | import usersFactory from 'test/factories/user'; 2 | import ordersFactory from 'test/factories/order'; 3 | import thingsFactory from 'test/factories/thing'; 4 | 5 | export { usersFactory, thingsFactory, ordersFactory }; 6 | -------------------------------------------------------------------------------- /src/server/app/auth/auth.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, Res } from '@nestjs/common'; 2 | 3 | @Controller('auth') 4 | export class AuthController { 5 | @Get() 6 | async auth(@Res() res) { 7 | return res.redirect('/auth/cognito'); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /test/jest-request.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": ["js", "json", "ts"], 3 | "modulePaths": [ 4 | "../" 5 | ], 6 | "testEnvironment": "node", 7 | "testRegex": ".request-spec.ts$", 8 | "transform": { 9 | "^.+\\.(t|j)s$": "ts-jest" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/server/common/types/user.ts: -------------------------------------------------------------------------------- 1 | export type Provider = 'google' | 'cognito'; 2 | 3 | export class User { 4 | id: number; 5 | provider: Provider; 6 | providerId: string; 7 | username: string; 8 | name?: string; 9 | created_at: Date; 10 | updated_at: Date; 11 | } 12 | -------------------------------------------------------------------------------- /test/factories/thing.ts: -------------------------------------------------------------------------------- 1 | import { Factory } from 'fishery'; 2 | import { faker } from '@faker-js/faker'; 3 | 4 | import { CreateThingDto } from 'src/server/app/things/dto/create-thing.dto'; 5 | 6 | export default Factory.define(() => ({ 7 | name: faker.lorem.words(), 8 | })); 9 | -------------------------------------------------------------------------------- /src/server/server.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | 3 | import { AppModule } from 'src/server/app/app.module'; 4 | import { ViewModule } from 'src/server/view/view.module'; 5 | 6 | @Module({ 7 | imports: [AppModule, ViewModule], 8 | }) 9 | export class ServerModule {} 10 | -------------------------------------------------------------------------------- /cypress/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "baseUrl": "../node_modules", 5 | "types": ["cypress"], 6 | "isolatedModules": false 7 | }, 8 | "include": [ 9 | "**/*.ts", 10 | "../cypress.config.ts", 11 | "../cypress.docker.config.ts" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /src/server/view/view.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | 3 | import { ViewController } from './view.controller'; 4 | import { ViewService } from './view.service'; 5 | 6 | @Module({ 7 | imports: [], 8 | providers: [ViewService], 9 | controllers: [ViewController], 10 | }) 11 | export class ViewModule {} 12 | -------------------------------------------------------------------------------- /scripts/web-docker-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | wait_for() 5 | { 6 | echo "Waiting $1 seconds for $2:$3" 7 | timeout $1 sh -c 'until nc -z $0 $1; do sleep 0.1; done' $2 $3 || return 1 8 | echo "$2:$3 available" 9 | } 10 | 11 | yarn install 12 | 13 | wait_for 10 db 5432 14 | 15 | yarn typeorm migration:run 16 | 17 | yarn console seed 18 | 19 | exec "$@" 20 | -------------------------------------------------------------------------------- /src/server/app/orders/dto/create-order.dto.ts: -------------------------------------------------------------------------------- 1 | import { Thing } from '../../things/thing.entity'; 2 | import { User } from '../../users/user.entity'; 3 | 4 | export class CreateOrderDto { 5 | alias: string; 6 | user: User; 7 | thing: Thing; 8 | } 9 | 10 | export class CreateOrderFromThingDetailsDto { 11 | alias: string; 12 | user: User; 13 | thingName: string; 14 | } 15 | -------------------------------------------------------------------------------- /test/factories/order.ts: -------------------------------------------------------------------------------- 1 | import { Factory } from 'fishery'; 2 | import { faker } from '@faker-js/faker'; 3 | 4 | import { CreateOrderDto } from 'src/server/app/orders/dto/create-order.dto'; 5 | 6 | export default Factory.define(({ associations }) => ({ 7 | alias: faker.internet.userName(), 8 | user: associations.user, 9 | thing: associations.thing, 10 | })); 11 | -------------------------------------------------------------------------------- /src/server/main.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core'; 2 | import * as cookieParser from 'cookie-parser'; 3 | 4 | import { ServerModule } from 'src/server/server.module'; 5 | 6 | async function bootstrap() { 7 | const app = await NestFactory.create(ServerModule); 8 | app.use(cookieParser()); 9 | 10 | await app.listen(process.env.PORT || 3000); 11 | } 12 | bootstrap(); 13 | -------------------------------------------------------------------------------- /src/server/app/auth/graphql/gql-auth.decorator.ts: -------------------------------------------------------------------------------- 1 | import { createParamDecorator, ExecutionContext } from '@nestjs/common'; 2 | import { GqlExecutionContext } from '@nestjs/graphql'; 3 | 4 | export const CurrentUser = createParamDecorator( 5 | (_data: unknown, context: ExecutionContext) => { 6 | const ctx = GqlExecutionContext.create(context); 7 | return ctx.getContext().req.user; 8 | }, 9 | ); 10 | -------------------------------------------------------------------------------- /cypress/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: 'cypress/tsconfig.json', 5 | sourceType: 'module', 6 | }, 7 | plugins: ['@typescript-eslint/eslint-plugin'], 8 | extends: [ 9 | 'plugin:@typescript-eslint/recommended', 10 | 'plugin:prettier/recommended', 11 | ], 12 | root: true, 13 | ignorePatterns: ['.eslintrc.js'], 14 | }; 15 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | Adopted from [Ruby CoC](https://www.ruby-lang.org/en/conduct/). 2 | 3 | - Participants will be tolerant of opposing views. 4 | - Participants must ensure that their language and actions are free of personal attacks and disparaging personal remarks. 5 | - When interpreting the words and actions of others, participants should always assume good intentions. 6 | - Behaviour which can be reasonably considered harassment will not be tolerated. 7 | -------------------------------------------------------------------------------- /src/server/app/auth/graphql/gql-auth.guard.ts: -------------------------------------------------------------------------------- 1 | import { ExecutionContext, Injectable } from '@nestjs/common'; 2 | import { GqlExecutionContext } from '@nestjs/graphql'; 3 | import { AuthGuard } from '@nestjs/passport'; 4 | 5 | @Injectable() 6 | export class GqlAuthGuard extends AuthGuard('jwt') { 7 | getRequest(context: ExecutionContext) { 8 | const ctx = GqlExecutionContext.create(context); 9 | return ctx.getContext().req; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /test/factories/user.ts: -------------------------------------------------------------------------------- 1 | import { Factory } from 'fishery'; 2 | import { faker } from '@faker-js/faker'; 3 | 4 | import { CreateUserDto } from 'src/server/app/users/dto/create-user.dto'; 5 | 6 | export default Factory.define(() => ({ 7 | provider: faker.helpers.arrayElement(['google', 'cognito']), 8 | providerId: faker.datatype.hexadecimal(10), 9 | username: faker.internet.email(), 10 | name: faker.name.findName(), 11 | })); 12 | -------------------------------------------------------------------------------- /src/server/app/users/users.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { TypeOrmModule } from '@nestjs/typeorm'; 3 | import { User } from './user.entity'; 4 | import { UsersResolver } from './users.resolver'; 5 | import { UsersService } from './users.service'; 6 | 7 | @Module({ 8 | imports: [TypeOrmModule.forFeature([User])], 9 | providers: [UsersService, UsersResolver], 10 | exports: [UsersService], 11 | }) 12 | export class UsersModule {} 13 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | DATABASE_URL=postgres://postgres:@db:5432 2 | JWT_SECRET=secret 3 | JWT_EXPIRES_IN=10d 4 | OAUTH_GOOGLE_ID=id 5 | OAUTH_GOOGLE_SECRET=secret 6 | OAUTH_GOOGLE_REDIRECT_URL=http://localhost:3000/auth/google/redirect 7 | OAUTH_COGNITO_ID=id 8 | OAUTH_COGNITO_SECRET=secret 9 | OAUTH_COGNITO_REDIRECT_URL=http://localhost:3000/auth/cognito/redirect 10 | OAUTH_COGNITO_DOMAIN=nestjs-playground-dev 11 | OAUTH_COGNITO_REGION=us-east-1 12 | NEXT_PUBLIC_GA_MEASUREMENT_ID= 13 | -------------------------------------------------------------------------------- /src/server/app/things/things.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { TypeOrmModule } from '@nestjs/typeorm'; 3 | 4 | import { Thing } from './thing.entity'; 5 | import { ThingsResolver } from './things.resolver'; 6 | import { ThingsService } from './things.service'; 7 | 8 | @Module({ 9 | imports: [TypeOrmModule.forFeature([Thing])], 10 | providers: [ThingsService, ThingsResolver], 11 | exports: [ThingsService], 12 | }) 13 | export class ThingsModule {} 14 | -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from '@jest/types'; 2 | 3 | const config: Config.InitialOptions = { 4 | moduleFileExtensions: ['js', 'json', 'ts'], 5 | modulePaths: ['.'], 6 | testRegex: '.*\\.spec\\.ts$', 7 | transform: { 8 | '^.+\\.ts$': 'ts-jest', 9 | }, 10 | collectCoverageFrom: ['src/server/**/*.ts'], 11 | coveragePathIgnorePatterns: ['src/server/console', 'src/server/migration'], 12 | coverageDirectory: 'coverage', 13 | testEnvironment: 'node', 14 | }; 15 | 16 | export default config; 17 | -------------------------------------------------------------------------------- /ormconfig.ts: -------------------------------------------------------------------------------- 1 | import { DataSource } from 'typeorm'; 2 | import * as dotenv from 'dotenv'; 3 | 4 | dotenv.config(); 5 | 6 | const source = new DataSource({ 7 | type: 'postgres' as const, 8 | url: process.env.DATABASE_URL, 9 | entities: ['src/server/app/**/*.entity.ts'], 10 | migrations: ['src/server/migration/*.{ts,js}'], 11 | extra: { 12 | ssl: 13 | process.env.NODE_ENV === 'production' 14 | ? { rejectUnauthorized: false } 15 | : false, 16 | }, 17 | }); 18 | 19 | export default source; 20 | -------------------------------------------------------------------------------- /src/server/app/orders/orders.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { TypeOrmModule } from '@nestjs/typeorm'; 3 | 4 | import { OrdersService } from './orders.service'; 5 | import { OrdersResolver } from './orders.resolver'; 6 | import { Order } from './order.entity'; 7 | import { ThingsModule } from '../things/things.module'; 8 | 9 | @Module({ 10 | imports: [TypeOrmModule.forFeature([Order]), ThingsModule], 11 | providers: [OrdersService, OrdersResolver], 12 | }) 13 | export class OrdersModule {} 14 | -------------------------------------------------------------------------------- /test/utils.ts: -------------------------------------------------------------------------------- 1 | import * as request from 'supertest'; 2 | 3 | import { JwtAuthService } from 'src/server/app/auth/jwt/jwt-auth.service'; 4 | import { User } from 'src/server/app/users/user.entity'; 5 | import { SESSION_COOKIE_KEY } from 'src/server/config/constants'; 6 | 7 | export const login = ( 8 | agent: request.Test, 9 | user: User, 10 | authService: JwtAuthService, 11 | ) => { 12 | const jwtToken = authService.login(user).accessToken; 13 | return agent.set('Cookie', [`${SESSION_COOKIE_KEY}=${jwtToken}`]); 14 | }; 15 | -------------------------------------------------------------------------------- /src/server/app/auth/google/google-oauth.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { UsersModule } from '../../users/users.module'; 3 | import { JwtAuthModule } from '../jwt/jwt-auth.module'; 4 | import { GoogleOauthController } from './google-oauth.controller'; 5 | import { GoogleOauthStrategy } from './google-oauth.strategy'; 6 | 7 | @Module({ 8 | imports: [UsersModule, JwtAuthModule], 9 | controllers: [GoogleOauthController], 10 | providers: [GoogleOauthStrategy], 11 | }) 12 | export class GoogleOauthModule {} 13 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "allowSyntheticDefaultImports": true, 9 | "target": "es2017", 10 | "sourceMap": true, 11 | "outDir": "./dist", 12 | "baseUrl": "./", 13 | "incremental": true 14 | }, 15 | "ts-node": { 16 | "files": true 17 | }, 18 | "include": ["src/server", "test", "ormconfig.ts", "jest.config.ts"], 19 | } 20 | -------------------------------------------------------------------------------- /src/client/pages/profile.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { NextPage } from 'next'; 3 | import { Request } from 'express'; 4 | 5 | export async function getServerSideProps({ req }) { 6 | return { 7 | props: { user: (req as Request).user }, 8 | }; 9 | } 10 | 11 | type Props = ExtractPromiseType>; 12 | 13 | const Profile: NextPage = (props) => { 14 | const { user } = props; 15 | 16 | return

Profile {JSON.stringify(user)}

; 17 | }; 18 | 19 | export default Profile; 20 | -------------------------------------------------------------------------------- /src/server/app/auth/cognito/cognito-oauth.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { UsersModule } from '../../users/users.module'; 3 | import { JwtAuthModule } from '../jwt/jwt-auth.module'; 4 | import { CognitoOauthController } from './cognito-oauth.controller'; 5 | import { CognitoOauthStrategy } from './cognito-oauth.strategy'; 6 | 7 | @Module({ 8 | imports: [UsersModule, JwtAuthModule], 9 | controllers: [CognitoOauthController], 10 | providers: [CognitoOauthStrategy], 11 | }) 12 | export class CognitoOauthModule {} 13 | -------------------------------------------------------------------------------- /src/server/app/auth/jwt/jwt-auth.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { JwtService } from '@nestjs/jwt'; 3 | import { User } from '../../users/user.entity'; 4 | import { JwtPayload } from './jwt-auth.strategy'; 5 | 6 | @Injectable() 7 | export class JwtAuthService { 8 | constructor(private jwtService: JwtService) {} 9 | 10 | login(user: User) { 11 | const payload: JwtPayload = { username: user.username, sub: user.id }; 12 | return { 13 | accessToken: this.jwtService.sign(payload), 14 | }; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.github/workflows/dependabot.yml: -------------------------------------------------------------------------------- 1 | name: Dependabot auto-merge 2 | on: pull_request 3 | 4 | permissions: 5 | contents: write 6 | 7 | jobs: 8 | dependabot: 9 | runs-on: ubuntu-latest 10 | if: ${{ github.actor == 'dependabot[bot]' }} 11 | steps: 12 | - name: Enable auto-merge for Dependabot PRs 13 | run: gh pr merge --auto --merge "$PR_URL" 14 | env: 15 | PR_URL: ${{github.event.pull_request.html_url}} 16 | # Using GITHUB_TOKEN will not trigger a build after merge 17 | GH_TOKEN: ${{secrets.GITHUB_TOKEN}} 18 | -------------------------------------------------------------------------------- /src/server/app/app.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Request, Get, UseGuards } from '@nestjs/common'; 2 | 3 | import { AppService } from './app.service'; 4 | import { JwtAuthGuard } from './auth/jwt/jwt-auth.guard'; 5 | 6 | @Controller() 7 | export class AppController { 8 | constructor(private readonly appService: AppService) {} 9 | 10 | @Get('/') 11 | getHello(): string { 12 | return this.appService.getHello(); 13 | } 14 | 15 | @UseGuards(JwtAuthGuard) 16 | @Get('private') 17 | getPrivate(@Request() req) { 18 | return req.user; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/client/pages/home.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { NextPage } from 'next'; 3 | 4 | const Home: NextPage<{ message: string; query: string }> = (props) => { 5 | const { message, query } = props; 6 | 7 | return ( 8 |
9 |

Hello from NextJS! - Home

10 | {message} 11 | {query} 12 |
13 | ); 14 | }; 15 | 16 | Home.getInitialProps = ({ query }) => { 17 | return { 18 | message: 'some initial props including query params', 19 | query: JSON.stringify(query), 20 | }; 21 | }; 22 | 23 | export default Home; 24 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | web: 5 | build: 6 | context: . 7 | image: ${NESTJS_STARTER_IMAGE:-nestjs-starter:local} 8 | entrypoint: scripts/web-docker-entrypoint.sh 9 | command: yarn start:dev 10 | ports: 11 | - "3000:3000" 12 | depends_on: 13 | - db 14 | volumes: 15 | - ${DUMMY_MOUNT:-.:/app} 16 | 17 | db: 18 | image: postgres:13.4-alpine 19 | environment: 20 | # Login without password 21 | POSTGRES_HOST_AUTH_METHOD: "trust" 22 | # Auto-create test DB 23 | POSTGRES_DB: test 24 | -------------------------------------------------------------------------------- /src/server/app/things/things.resolver.ts: -------------------------------------------------------------------------------- 1 | import { Resolver, Query } from '@nestjs/graphql'; 2 | import { Inject } from '@nestjs/common'; 3 | import { FindManyOptions } from 'typeorm'; 4 | import { Thing } from './thing.entity'; 5 | import { ThingsService } from './things.service'; 6 | 7 | @Resolver((_of) => Thing) 8 | export class ThingsResolver { 9 | constructor(@Inject(ThingsService) private thingsService: ThingsService) {} 10 | 11 | @Query((_returns) => [Thing]) 12 | async things(params: FindManyOptions = {}): Promise { 13 | return this.thingsService.findAll(params); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/server/migration/1643046828766-MakeUserNameOptional.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | 3 | export class MakeUserNameOptional1643046828766 implements MigrationInterface { 4 | name = 'MakeUserNameOptional1643046828766'; 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query( 8 | `ALTER TABLE "user" ALTER COLUMN "name" DROP NOT NULL`, 9 | ); 10 | } 11 | 12 | public async down(queryRunner: QueryRunner): Promise { 13 | await queryRunner.query( 14 | `ALTER TABLE "user" ALTER COLUMN "name" SET NOT NULL`, 15 | ); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/server/app/auth/auth.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { UsersModule } from '../users/users.module'; 3 | import { PassportModule } from '@nestjs/passport'; 4 | import { GoogleOauthModule } from './google/google-oauth.module'; 5 | import { JwtAuthModule } from './jwt/jwt-auth.module'; 6 | import { AuthController } from './auth.controller'; 7 | import { CognitoOauthModule } from './cognito/cognito-oauth.module'; 8 | 9 | @Module({ 10 | controllers: [AuthController], 11 | imports: [ 12 | UsersModule, 13 | PassportModule, 14 | GoogleOauthModule, 15 | JwtAuthModule, 16 | CognitoOauthModule, 17 | ], 18 | }) 19 | export class AuthModule {} 20 | -------------------------------------------------------------------------------- /src/server/app/app.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { AppController } from './app.controller'; 3 | import { AppService } from './app.service'; 4 | 5 | describe('AppController', () => { 6 | let app: TestingModule; 7 | 8 | beforeAll(async () => { 9 | app = await Test.createTestingModule({ 10 | controllers: [AppController], 11 | providers: [AppService], 12 | }).compile(); 13 | }); 14 | 15 | describe('getHello', () => { 16 | it('should return hello message', () => { 17 | const appController = app.get(AppController); 18 | expect(appController.getHello()).toBe('Hello from NestJS!'); 19 | }); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /src/client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "strict": false, 12 | "forceConsistentCasingInFileNames": true, 13 | "noEmit": true, 14 | "esModuleInterop": true, 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "jsx": "preserve", 20 | "incremental": true 21 | }, 22 | "include": [ 23 | "next-env.d.ts", 24 | "next.config.js", 25 | "app", 26 | "pages", 27 | "../server/common" 28 | ], 29 | "exclude": [ 30 | "node_modules" 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /cypress/support/e2e.ts: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/e2e.ts is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import './commands'; 18 | 19 | // Alternatively you can use CommonJS syntax: 20 | // require('./commands') 21 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: 'tsconfig.json', 5 | sourceType: 'module', 6 | }, 7 | plugins: ['@typescript-eslint/eslint-plugin'], 8 | extends: [ 9 | 'plugin:@typescript-eslint/recommended', 10 | 'plugin:prettier/recommended', 11 | ], 12 | root: true, 13 | env: { 14 | node: true, 15 | jest: true, 16 | }, 17 | ignorePatterns: ['.eslintrc.js'], 18 | rules: { 19 | '@typescript-eslint/interface-name-prefix': 'off', 20 | '@typescript-eslint/explicit-function-return-type': 'off', 21 | '@typescript-eslint/explicit-module-boundary-types': 'off', 22 | '@typescript-eslint/no-explicit-any': 'off', 23 | '@typescript-eslint/no-unused-vars': ['warn', { 'argsIgnorePattern': '^_' }] 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /src/server/app/users/users.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { InjectRepository } from '@nestjs/typeorm'; 3 | import { FindManyOptions, FindOneOptions, Repository } from 'typeorm'; 4 | 5 | import { CreateUserDto } from './dto/create-user.dto'; 6 | import { User } from './user.entity'; 7 | 8 | @Injectable() 9 | export class UsersService { 10 | constructor( 11 | @InjectRepository(User) 12 | private usersRepository: Repository, 13 | ) {} 14 | 15 | create(user: CreateUserDto) { 16 | return this.usersRepository.save(user); 17 | } 18 | 19 | findOne(params: FindOneOptions = {}) { 20 | return this.usersRepository.findOne(params); 21 | } 22 | 23 | findAll(params: FindManyOptions = {}) { 24 | return this.usersRepository.find(params); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/server/app/things/thing.entity.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Entity, 3 | Column, 4 | PrimaryGeneratedColumn, 5 | CreateDateColumn, 6 | UpdateDateColumn, 7 | OneToMany, 8 | } from 'typeorm'; 9 | import { ObjectType, Field } from '@nestjs/graphql'; 10 | import { Order } from '../orders/order.entity'; 11 | 12 | @ObjectType() 13 | @Entity() 14 | export class Thing { 15 | @Field() 16 | @PrimaryGeneratedColumn() 17 | id: number; 18 | 19 | @Field() 20 | @Column({ nullable: false }) 21 | name: string; 22 | 23 | @Field((_type) => [Order], { nullable: 'items' }) 24 | @OneToMany((_type) => Order, (order) => order.thing) 25 | orders?: Order[]; 26 | 27 | @Field() 28 | @Column() 29 | @CreateDateColumn() 30 | created_at: Date; 31 | 32 | @Field() 33 | @Column() 34 | @UpdateDateColumn() 35 | updated_at: Date; 36 | } 37 | -------------------------------------------------------------------------------- /src/server/console.ts: -------------------------------------------------------------------------------- 1 | import { BootstrapConsole } from 'nestjs-console'; 2 | import { AppModule } from 'src/server/app/app.module'; 3 | 4 | const bootstrap = new BootstrapConsole({ 5 | module: AppModule, 6 | useDecorators: true, 7 | }); 8 | bootstrap.init().then(async (app) => { 9 | try { 10 | // init your app 11 | await app.init(); 12 | // boot the cli 13 | await bootstrap.boot(); 14 | 15 | // Use app.close() instead of process.exit() because app.close() will 16 | // trigger onModuleDestroy, beforeApplicationShutdown and onApplicationShutdown. 17 | // For example, in your command doing the database operation and need to close 18 | // when error or finish. 19 | app.close(); 20 | 21 | process.exit(0); 22 | } catch (e) { 23 | app.close(); 24 | 25 | process.exit(1); 26 | } 27 | }); 28 | -------------------------------------------------------------------------------- /src/server/app/things/things.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { InjectRepository } from '@nestjs/typeorm'; 3 | import { FindManyOptions, Repository, FindOneOptions } from 'typeorm'; 4 | 5 | import { Thing } from './thing.entity'; 6 | import { CreateThingDto } from './dto/create-thing.dto'; 7 | 8 | @Injectable() 9 | export class ThingsService { 10 | constructor( 11 | @InjectRepository(Thing) 12 | private thingsRepository: Repository, 13 | ) {} 14 | 15 | create(thing: CreateThingDto) { 16 | return this.thingsRepository.save(thing); 17 | } 18 | 19 | findOne(params: FindOneOptions = {}) { 20 | return this.thingsRepository.findOne(params); 21 | } 22 | 23 | findAll(params: FindManyOptions = {}) { 24 | return this.thingsRepository.find(params); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/server/app/auth/jwt/jwt-auth.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { ConfigService } from '@nestjs/config'; 3 | import { JwtModule } from '@nestjs/jwt'; 4 | import { JwtAuthService } from './jwt-auth.service'; 5 | import { JwtAuthStrategy } from './jwt-auth.strategy'; 6 | 7 | @Module({ 8 | imports: [ 9 | JwtModule.registerAsync({ 10 | useFactory: async (configService: ConfigService) => { 11 | return { 12 | secret: configService.get('JWT_SECRET'), 13 | signOptions: { 14 | expiresIn: configService.get('JWT_EXPIRES_IN'), 15 | }, 16 | }; 17 | }, 18 | inject: [ConfigService], 19 | }), 20 | ], 21 | providers: [JwtAuthStrategy, JwtAuthService], 22 | exports: [JwtModule, JwtAuthService], 23 | }) 24 | export class JwtAuthModule {} 25 | -------------------------------------------------------------------------------- /src/server/migration/1619278600863-CreateUsers.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | 3 | export class CreateUsers1619278600863 implements MigrationInterface { 4 | name = 'CreateUsers1619278600863'; 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query( 8 | `CREATE TABLE IF NOT EXISTS "user" ("id" SERIAL NOT NULL, "provider" character varying NOT NULL, "providerId" character varying NOT NULL, "username" character varying NOT NULL, "name" character varying NOT NULL, "created_at" TIMESTAMP NOT NULL DEFAULT now(), "updated_at" TIMESTAMP NOT NULL DEFAULT now(), CONSTRAINT "PK_cace4a159ff9f2512dd42373760" PRIMARY KEY ("id"))`, 9 | ); 10 | } 11 | 12 | public async down(queryRunner: QueryRunner): Promise { 13 | await queryRunner.query(`DROP TABLE "user"`); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/client/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { AppProps } from 'next/app'; 3 | import Script from 'next/script'; 4 | 5 | const App = ({ Component, pageProps }: AppProps) => { 6 | return ( 7 | <> 8 | 21 | 22 | 23 | 24 | ); 25 | }; 26 | 27 | export default App; 28 | -------------------------------------------------------------------------------- /src/client/pages/orders.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import { NextPage } from 'next'; 3 | import { Request } from 'express'; 4 | 5 | import { typedQuery } from '../app/apollo-client'; 6 | 7 | export async function getServerSideProps({ req }) { 8 | const { data } = await typedQuery( 9 | { orders: { alias: true, thing: { name: true } } }, 10 | req, 11 | ); 12 | 13 | return { 14 | props: { user: (req as Request).user, orders: data.orders }, 15 | }; 16 | } 17 | 18 | type Props = ExtractPromiseType>; 19 | 20 | const Orders: NextPage = (props) => { 21 | useEffect(() => { 22 | window.gtag('event', 'ordersOpened'); 23 | }, []); 24 | 25 | return ( 26 |
27 |

Orders overview

28 | {JSON.stringify(props)} 29 |
30 | ); 31 | }; 32 | 33 | export default Orders; 34 | -------------------------------------------------------------------------------- /src/server/view/view.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, Res, Req, UseGuards } from '@nestjs/common'; 2 | import { Request, Response } from 'express'; 3 | import { JwtAuthGuard } from '../app/auth/jwt/jwt-auth.guard'; 4 | 5 | import { ViewService } from './view.service'; 6 | 7 | @Controller('/') 8 | export class ViewController { 9 | constructor(private viewService: ViewService) {} 10 | 11 | @Get('_next*') 12 | public async assets(@Req() req: Request, @Res() res: Response) { 13 | await this.viewService.handler(req, res); 14 | } 15 | @Get('home') 16 | public async home(@Req() req: Request, @Res() res: Response) { 17 | await this.viewService.handler(req, res); 18 | } 19 | 20 | @UseGuards(JwtAuthGuard) 21 | @Get('/:path((?!graphql$))*') 22 | public async authenticatedPage(@Req() req: Request, @Res() res: Response) { 23 | await this.viewService.handler(req, res); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/server/view/view.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, OnModuleInit } from '@nestjs/common'; 2 | import { ConfigService } from '@nestjs/config'; 3 | import createServer from 'next'; 4 | import { Request, Response } from 'express'; 5 | import { NextServer } from 'next/dist/server/next'; 6 | 7 | @Injectable() 8 | export class ViewService implements OnModuleInit { 9 | private server: NextServer; 10 | 11 | constructor(private configService: ConfigService) {} 12 | 13 | async onModuleInit(): Promise { 14 | try { 15 | this.server = createServer({ 16 | dev: this.configService.get('NODE_ENV') !== 'production', 17 | dir: './src/client', 18 | }); 19 | await this.server.prepare(); 20 | } catch (error) { 21 | console.error(error); 22 | } 23 | } 24 | 25 | handler(req: Request, res: Response) { 26 | return this.server.getRequestHandler()(req, res); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/server/app/users/users.resolver.ts: -------------------------------------------------------------------------------- 1 | import { Resolver, Query } from '@nestjs/graphql'; 2 | import { Inject, UseGuards } from '@nestjs/common'; 3 | import { UsersService } from './users.service'; 4 | import { FindManyOptions } from 'typeorm'; 5 | import { User } from './user.entity'; 6 | import { GqlAuthGuard } from '../auth/graphql/gql-auth.guard'; 7 | import { CurrentUser } from '../auth/graphql/gql-auth.decorator'; 8 | 9 | @Resolver((_of) => User) 10 | export class UsersResolver { 11 | constructor(@Inject(UsersService) private usersService: UsersService) {} 12 | 13 | @Query((_returns) => [User]) 14 | async users(params: FindManyOptions = {}): Promise { 15 | return this.usersService.findAll(params); 16 | } 17 | 18 | @Query((_returns) => User) 19 | @UseGuards(GqlAuthGuard) 20 | whoAmI(@CurrentUser() user: User) { 21 | return this.usersService.findOne({ where: { id: user.id } }); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/client/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: 'src/client/tsconfig.json', 5 | sourceType: 'module', 6 | }, 7 | plugins: ['@typescript-eslint/eslint-plugin'], 8 | extends: [ 9 | 'plugin:@typescript-eslint/recommended', 10 | 'plugin:prettier/recommended', 11 | 'plugin:@next/next/recommended', 12 | ], 13 | root: true, 14 | env: { 15 | jest: true, 16 | }, 17 | ignorePatterns: ['.eslintrc.js'], 18 | rules: { 19 | '@typescript-eslint/interface-name-prefix': 'off', 20 | '@typescript-eslint/explicit-function-return-type': 'off', 21 | '@typescript-eslint/explicit-module-boundary-types': 'off', 22 | '@typescript-eslint/no-explicit-any': 'off', 23 | '@typescript-eslint/no-unused-vars': ['warn', { 'argsIgnorePattern': '^_' }], 24 | "@next/next/no-html-link-for-pages": ["error", "src/client/pages/"], // custom pages path 25 | }, 26 | }; 27 | -------------------------------------------------------------------------------- /src/server/app/auth/google/google-oauth.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, Req, Res, UseGuards } from '@nestjs/common'; 2 | import { Request, Response } from 'express'; 3 | import { GoogleOauthGuard } from './google-oauth.guard'; 4 | import { JwtAuthService } from '../jwt/jwt-auth.service'; 5 | import { SESSION_COOKIE_KEY } from 'src/server/config/constants'; 6 | 7 | @Controller('auth/google') 8 | export class GoogleOauthController { 9 | constructor(private jwtAuthService: JwtAuthService) {} 10 | 11 | @Get() 12 | @UseGuards(GoogleOauthGuard) 13 | async googleAuth(@Req() _req) { 14 | // Guard redirects 15 | } 16 | 17 | @Get('redirect') 18 | @UseGuards(GoogleOauthGuard) 19 | async googleAuthRedirect(@Req() req: Request, @Res() res: Response) { 20 | const { accessToken } = this.jwtAuthService.login(req.user); 21 | res.cookie(SESSION_COOKIE_KEY, accessToken, { 22 | httpOnly: true, 23 | sameSite: 'lax', 24 | }); 25 | return res.redirect('/profile'); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/server/app/orders/order.entity.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Entity, 3 | Column, 4 | PrimaryGeneratedColumn, 5 | CreateDateColumn, 6 | UpdateDateColumn, 7 | ManyToOne, 8 | } from 'typeorm'; 9 | import { ObjectType, Field } from '@nestjs/graphql'; 10 | import { User } from '../users/user.entity'; 11 | import { Thing } from '../things/thing.entity'; 12 | 13 | @ObjectType() 14 | @Entity() 15 | export class Order { 16 | @Field() 17 | @PrimaryGeneratedColumn() 18 | id: number; 19 | 20 | @Field() 21 | @Column({ nullable: false }) 22 | alias: string; 23 | 24 | @Field((_type) => User) 25 | @ManyToOne((_type) => User, (user) => user.orders, { nullable: false }) 26 | user: User; 27 | 28 | @Field((_type) => Thing) 29 | @ManyToOne((_type) => Thing, (thing) => thing.orders, { nullable: false }) 30 | thing: Thing; 31 | 32 | @Field() 33 | @Column() 34 | @CreateDateColumn() 35 | created_at: Date; 36 | 37 | @Field() 38 | @Column() 39 | @UpdateDateColumn() 40 | updated_at: Date; 41 | } 42 | -------------------------------------------------------------------------------- /src/server/console/seed.service.ts: -------------------------------------------------------------------------------- 1 | import { Inject } from '@nestjs/common'; 2 | import { Console, Command, createSpinner } from 'nestjs-console'; 3 | import { ThingsService } from '../app/things/things.service'; 4 | 5 | @Console() 6 | export class SeedService { 7 | constructor(@Inject(ThingsService) private thingsService: ThingsService) {} 8 | 9 | @Command({ 10 | command: 'seed', 11 | description: 'Seed DB', 12 | }) 13 | async seed(): Promise { 14 | const spin = createSpinner(); 15 | 16 | spin.start('Seeding the DB'); 17 | 18 | await this.seedThings(); 19 | 20 | spin.succeed('Seeding done'); 21 | } 22 | 23 | async seedThings() { 24 | const things = [{ name: 'this is a thing you can order' }]; 25 | 26 | for (const thingParams of things) { 27 | const thing = await this.thingsService.findOne({ 28 | where: thingParams, 29 | }); 30 | if (!thing) { 31 | await this.thingsService.create(thingParams); 32 | } 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/server/app/auth/cognito/cognito-oauth.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, Req, Res, UseGuards } from '@nestjs/common'; 2 | import { Request, Response } from 'express'; 3 | import { SESSION_COOKIE_KEY } from 'src/server/config/constants'; 4 | import { JwtAuthService } from '../jwt/jwt-auth.service'; 5 | import { CognitoOauthGuard } from './cognito-oauth.guard'; 6 | 7 | @Controller('auth/cognito') 8 | export class CognitoOauthController { 9 | constructor(private jwtAuthService: JwtAuthService) {} 10 | 11 | @Get() 12 | @UseGuards(CognitoOauthGuard) 13 | async cognitoAuth(@Req() _req) { 14 | // Guard redirects 15 | } 16 | 17 | @Get('redirect') 18 | @UseGuards(CognitoOauthGuard) 19 | async cognitoAuthRedirect(@Req() req: Request, @Res() res: Response) { 20 | const { accessToken } = this.jwtAuthService.login(req.user); 21 | res.cookie(SESSION_COOKIE_KEY, accessToken, { 22 | httpOnly: true, 23 | sameSite: 'lax', 24 | }); 25 | return res.redirect('/profile'); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/server/app/auth/jwt/jwt-auth.strategy.ts: -------------------------------------------------------------------------------- 1 | import { Strategy } from 'passport-jwt'; 2 | import { PassportStrategy } from '@nestjs/passport'; 3 | import { Injectable } from '@nestjs/common'; 4 | import { ConfigService } from '@nestjs/config'; 5 | import { SESSION_COOKIE_KEY } from 'src/server/config/constants'; 6 | 7 | export type JwtPayload = { sub: number; username: string }; 8 | 9 | @Injectable() 10 | export class JwtAuthStrategy extends PassportStrategy(Strategy) { 11 | constructor(configService: ConfigService) { 12 | const extractJwtFromCookie = (req) => { 13 | let token = null; 14 | 15 | if (req && req.cookies) { 16 | token = req.cookies[SESSION_COOKIE_KEY]; 17 | } 18 | return token; 19 | }; 20 | 21 | super({ 22 | jwtFromRequest: extractJwtFromCookie, 23 | ignoreExpiration: false, 24 | secretOrKey: configService.get('JWT_SECRET'), 25 | }); 26 | } 27 | 28 | async validate(payload: JwtPayload) { 29 | return { id: payload.sub, username: payload.username }; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/schema.gql: -------------------------------------------------------------------------------- 1 | # ------------------------------------------------------ 2 | # THIS FILE WAS AUTOMATICALLY GENERATED (DO NOT MODIFY) 3 | # ------------------------------------------------------ 4 | 5 | type User { 6 | id: Float! 7 | provider: String! 8 | providerId: String! 9 | username: String! 10 | name: String! 11 | orders: [Order]! 12 | created_at: DateTime! 13 | updated_at: DateTime! 14 | } 15 | 16 | """ 17 | A date-time string at UTC, such as 2019-12-03T09:54:33Z, compliant with the date-time format. 18 | """ 19 | scalar DateTime 20 | 21 | type Order { 22 | id: Float! 23 | alias: String! 24 | user: User! 25 | thing: Thing! 26 | created_at: DateTime! 27 | updated_at: DateTime! 28 | } 29 | 30 | type Thing { 31 | id: Float! 32 | name: String! 33 | orders: [Order]! 34 | created_at: DateTime! 35 | updated_at: DateTime! 36 | } 37 | 38 | type Query { 39 | users: [User!]! 40 | whoAmI: User! 41 | things: [Thing!]! 42 | orders: [Order!]! 43 | } 44 | 45 | type Mutation { 46 | createOrder(thingName: String!, alias: String!): Order! 47 | } -------------------------------------------------------------------------------- /src/client/app/types/zeus/const.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | export const AllTypesProps: Record = { 4 | DateTime: `scalar.DateTime` as const, 5 | Mutation:{ 6 | createOrder:{ 7 | 8 | } 9 | } 10 | } 11 | 12 | export const ReturnTypes: Record = { 13 | User:{ 14 | id:"Float", 15 | provider:"String", 16 | providerId:"String", 17 | username:"String", 18 | name:"String", 19 | orders:"Order", 20 | created_at:"DateTime", 21 | updated_at:"DateTime" 22 | }, 23 | DateTime: `scalar.DateTime` as const, 24 | Order:{ 25 | id:"Float", 26 | alias:"String", 27 | user:"User", 28 | thing:"Thing", 29 | created_at:"DateTime", 30 | updated_at:"DateTime" 31 | }, 32 | Thing:{ 33 | id:"Float", 34 | name:"String", 35 | orders:"Order", 36 | created_at:"DateTime", 37 | updated_at:"DateTime" 38 | }, 39 | Query:{ 40 | users:"User", 41 | whoAmI:"User", 42 | things:"Thing", 43 | orders:"Order" 44 | }, 45 | Mutation:{ 46 | createOrder:"Order" 47 | } 48 | } 49 | 50 | export const Ops = { 51 | query: "Query" as const, 52 | mutation: "Mutation" as const 53 | } -------------------------------------------------------------------------------- /src/server/app/users/user.entity.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Entity, 3 | Column, 4 | PrimaryGeneratedColumn, 5 | CreateDateColumn, 6 | UpdateDateColumn, 7 | OneToMany, 8 | } from 'typeorm'; 9 | import { ObjectType, Field } from '@nestjs/graphql'; 10 | import { Provider } from 'src/server/common/types/user'; 11 | import { Order } from '../orders/order.entity'; 12 | 13 | @ObjectType() 14 | @Entity() 15 | export class User { 16 | @Field() 17 | @PrimaryGeneratedColumn() 18 | id: number; 19 | 20 | @Field() 21 | @Column({ nullable: false }) 22 | provider: Provider; 23 | 24 | @Field() 25 | @Column({ nullable: false }) 26 | providerId: string; 27 | 28 | @Field() 29 | @Column({ nullable: false }) 30 | username: string; 31 | 32 | @Field() 33 | @Column({ nullable: false }) 34 | name?: string; 35 | 36 | @Field((_type) => [Order], { nullable: 'items' }) 37 | @OneToMany((_type) => Order, (order) => order.user) 38 | orders?: Order[]; 39 | 40 | @Field() 41 | @Column() 42 | @CreateDateColumn() 43 | created_at: Date; 44 | 45 | @Field() 46 | @Column() 47 | @UpdateDateColumn() 48 | updated_at: Date; 49 | } 50 | -------------------------------------------------------------------------------- /src/client/app/apollo-client.ts: -------------------------------------------------------------------------------- 1 | import { ApolloClient, gql, InMemoryCache } from '@apollo/client'; 2 | import { Request } from 'express'; 3 | 4 | import { Zeus, ValueTypes, GraphQLTypes, InputType } from './types/zeus'; 5 | 6 | const client = new ApolloClient({ 7 | // TODO: make this configurable 8 | uri: 'http://localhost:3000/graphql', 9 | cache: new InMemoryCache(), 10 | }); 11 | 12 | export const context = (req: Request) => { 13 | return { headers: { Cookie: req.headers.cookie } }; 14 | }; 15 | 16 | export const typedQuery = ( 17 | query: Z | ValueTypes[O], 18 | req: Request, 19 | ) => { 20 | return client.query>({ 21 | query: gql(Zeus('query', query)), 22 | context: context(req), 23 | }); 24 | }; 25 | 26 | export const typedMutation = ( 27 | mutation: Z | ValueTypes[O], 28 | req: Request, 29 | ) => { 30 | return client.mutate>({ 31 | mutation: gql(Zeus('mutation', mutation)), 32 | context: context(req), 33 | }); 34 | }; 35 | 36 | export default client; 37 | -------------------------------------------------------------------------------- /src/server/app/auth/google/google-oauth.strategy.ts: -------------------------------------------------------------------------------- 1 | import { PassportStrategy } from '@nestjs/passport'; 2 | import { Profile, Strategy } from 'passport-google-oauth20'; 3 | import { Injectable } from '@nestjs/common'; 4 | import { ConfigService } from '@nestjs/config'; 5 | import { UsersService } from '../../users/users.service'; 6 | 7 | @Injectable() 8 | export class GoogleOauthStrategy extends PassportStrategy(Strategy, 'google') { 9 | constructor( 10 | configService: ConfigService, 11 | private readonly usersService: UsersService, 12 | ) { 13 | super({ 14 | clientID: configService.get('OAUTH_GOOGLE_ID'), 15 | clientSecret: configService.get('OAUTH_GOOGLE_SECRET'), 16 | callbackURL: configService.get('OAUTH_GOOGLE_REDIRECT_URL'), 17 | scope: ['email', 'profile'], 18 | }); 19 | } 20 | 21 | async validate( 22 | _accessToken: string, 23 | _refreshToken: string, 24 | profile: Profile, 25 | ) { 26 | const { id, name, emails } = profile; 27 | 28 | let user = await this.usersService.findOne({ 29 | where: { provider: 'google', providerId: id }, 30 | }); 31 | if (!user) { 32 | user = await this.usersService.create({ 33 | provider: 'google', 34 | providerId: id, 35 | name: name.givenName, 36 | username: emails[0].value, 37 | }); 38 | } 39 | 40 | return user; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /cypress/support/commands.ts: -------------------------------------------------------------------------------- 1 | /// 2 | // *********************************************** 3 | // This example commands.ts shows you how to 4 | // create various custom commands and overwrite 5 | // existing commands. 6 | // 7 | // For more comprehensive examples of custom 8 | // commands please read more here: 9 | // https://on.cypress.io/custom-commands 10 | // *********************************************** 11 | // 12 | // 13 | // -- This is a parent command -- 14 | // Cypress.Commands.add('login', (email, password) => { ... }) 15 | // 16 | // 17 | // -- This is a child command -- 18 | // Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... }) 19 | // 20 | // 21 | // -- This is a dual command -- 22 | // Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... }) 23 | // 24 | // 25 | // -- This will overwrite an existing command -- 26 | // Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) 27 | // 28 | // declare global { 29 | // namespace Cypress { 30 | // interface Chainable { 31 | // login(email: string, password: string): Chainable 32 | // drag(subject: string, options?: Partial): Chainable 33 | // dismiss(subject: string, options?: Partial): Chainable 34 | // visit(originalFn: CommandOriginalFn, url: string, options: Partial): Chainable 35 | // } 36 | // } 37 | // } 38 | -------------------------------------------------------------------------------- /src/server/app/orders/orders.resolver.ts: -------------------------------------------------------------------------------- 1 | import { Inject, UseGuards } from '@nestjs/common'; 2 | import { 3 | Args, 4 | Mutation, 5 | Parent, 6 | Query, 7 | ResolveField, 8 | Resolver, 9 | } from '@nestjs/graphql'; 10 | 11 | import { CurrentUser } from '../auth/graphql/gql-auth.decorator'; 12 | import { GqlAuthGuard } from '../auth/graphql/gql-auth.guard'; 13 | import { ThingsService } from '../things/things.service'; 14 | import { User } from '../users/user.entity'; 15 | import { Order } from './order.entity'; 16 | import { OrdersService } from './orders.service'; 17 | 18 | @Resolver((_of) => Order) 19 | export class OrdersResolver { 20 | constructor( 21 | @Inject(OrdersService) private ordersService: OrdersService, 22 | @Inject(ThingsService) private thingsService: ThingsService, 23 | ) {} 24 | 25 | @Query((_returns) => [Order]) 26 | @UseGuards(GqlAuthGuard) 27 | orders(@CurrentUser() user: User) { 28 | return this.ordersService.findAll({ where: { user: { id: user.id } } }); 29 | } 30 | 31 | @ResolveField() 32 | thing(@Parent() order: Order) { 33 | return this.thingsService.findOne({ 34 | where: { id: order.thing.id }, 35 | }); 36 | } 37 | 38 | @Mutation((_returns) => Order) 39 | @UseGuards(GqlAuthGuard) 40 | createOrder( 41 | @CurrentUser() user: User, 42 | @Args({ name: 'thingName', type: () => String }) thingName: string, 43 | @Args({ name: 'alias', type: () => String }) alias: string, 44 | ) { 45 | return this.ordersService.createFromThingDetails({ 46 | alias: alias, 47 | user: user, 48 | thingName: thingName, 49 | }); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/server/migration/1620037951321-AddThingsAndOrders.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | 3 | export class AddThingsAndOrders1620037951321 implements MigrationInterface { 4 | name = 'AddThingsAndOrders1620037951321'; 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query( 8 | `CREATE TABLE "thing" ("id" SERIAL NOT NULL, "name" character varying NOT NULL, "created_at" TIMESTAMP NOT NULL DEFAULT now(), "updated_at" TIMESTAMP NOT NULL DEFAULT now(), CONSTRAINT "PK_e7757c5911e20acd09faa22d1ac" PRIMARY KEY ("id"))`, 9 | ); 10 | await queryRunner.query( 11 | `CREATE TABLE "order" ("id" SERIAL NOT NULL, "alias" character varying NOT NULL, "created_at" TIMESTAMP NOT NULL DEFAULT now(), "updated_at" TIMESTAMP NOT NULL DEFAULT now(), "userId" integer, "thingId" integer, CONSTRAINT "PK_1031171c13130102495201e3e20" PRIMARY KEY ("id"))`, 12 | ); 13 | await queryRunner.query( 14 | `ALTER TABLE "order" ADD CONSTRAINT "FK_caabe91507b3379c7ba73637b84" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, 15 | ); 16 | await queryRunner.query( 17 | `ALTER TABLE "order" ADD CONSTRAINT "FK_cea8091f3cb66cba2b985c520f5" FOREIGN KEY ("thingId") REFERENCES "thing"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, 18 | ); 19 | } 20 | 21 | public async down(queryRunner: QueryRunner): Promise { 22 | await queryRunner.query( 23 | `ALTER TABLE "order" DROP CONSTRAINT "FK_cea8091f3cb66cba2b985c520f5"`, 24 | ); 25 | await queryRunner.query( 26 | `ALTER TABLE "order" DROP CONSTRAINT "FK_caabe91507b3379c7ba73637b84"`, 27 | ); 28 | await queryRunner.query(`DROP TABLE "order"`); 29 | await queryRunner.query(`DROP TABLE "thing"`); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/server/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { GraphQLModule } from '@nestjs/graphql'; 3 | import { TypeOrmModule } from '@nestjs/typeorm'; 4 | import { ConsoleModule } from 'nestjs-console'; 5 | import { join } from 'path'; 6 | import { ConfigModule, ConfigService } from '@nestjs/config'; 7 | import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo'; 8 | 9 | import { SeedService } from 'src/server/console/seed.service'; 10 | import { AppController } from './app.controller'; 11 | import { AppService } from './app.service'; 12 | import { AuthModule } from './auth/auth.module'; 13 | import { UsersModule } from './users/users.module'; 14 | import { ThingsModule } from './things/things.module'; 15 | import { OrdersModule } from './orders/orders.module'; 16 | 17 | @Module({ 18 | imports: [ 19 | ConfigModule.forRoot({ 20 | isGlobal: true, 21 | }), 22 | GraphQLModule.forRoot({ 23 | driver: ApolloDriver, 24 | autoSchemaFile: join(process.cwd(), 'src/schema.gql'), 25 | playground: { settings: { 'request.credentials': 'include' } }, 26 | }), 27 | TypeOrmModule.forRootAsync({ 28 | useFactory: async (configService: ConfigService) => ({ 29 | type: 'postgres', 30 | url: configService.get('DATABASE_URL'), 31 | autoLoadEntities: true, 32 | ssl: 33 | configService.get('NODE_ENV') === 'production' 34 | ? { rejectUnauthorized: false } 35 | : false, 36 | }), 37 | inject: [ConfigService], 38 | }), 39 | ConsoleModule, 40 | AuthModule, 41 | UsersModule, 42 | ThingsModule, 43 | OrdersModule, 44 | ], 45 | providers: [SeedService, AppService], 46 | controllers: [AppController], 47 | }) 48 | export class AppModule {} 49 | -------------------------------------------------------------------------------- /src/server/app/orders/orders.service.ts: -------------------------------------------------------------------------------- 1 | import { Inject, Injectable } from '@nestjs/common'; 2 | import { InjectRepository } from '@nestjs/typeorm'; 3 | import { FindManyOptions, FindOneOptions, Repository } from 'typeorm'; 4 | import { ThingsService } from '../things/things.service'; 5 | import { 6 | CreateOrderDto, 7 | CreateOrderFromThingDetailsDto, 8 | } from './dto/create-order.dto'; 9 | import { Order } from './order.entity'; 10 | 11 | @Injectable() 12 | export class OrdersService { 13 | constructor( 14 | @InjectRepository(Order) 15 | private ordersRepository: Repository, 16 | @Inject(ThingsService) private thingsService: ThingsService, 17 | ) {} 18 | 19 | create(order: CreateOrderDto) { 20 | return this.ordersRepository.save(order); 21 | } 22 | 23 | findOne(params: FindOneOptions = {}) { 24 | return this.ordersRepository.findOne( 25 | Object.assign({ relations: ['user', 'thing'] }, params), 26 | ); 27 | } 28 | 29 | findAll(params: FindManyOptions = {}) { 30 | return this.ordersRepository.find( 31 | Object.assign({ relations: ['user', 'thing'] }, params), 32 | ); 33 | } 34 | 35 | async findOrCreateOne(params: FindOneOptions = {}) { 36 | let order: Order; 37 | 38 | order = await this.findOne(params); 39 | if (!order) { 40 | const conditions = params.where as CreateOrderDto; 41 | order = await this.create({ 42 | alias: conditions.alias, 43 | user: conditions.user, 44 | thing: conditions.thing, 45 | }); 46 | } 47 | 48 | return order; 49 | } 50 | 51 | async createFromThingDetails(params: CreateOrderFromThingDetailsDto) { 52 | const thing = await this.thingsService.findOne({ 53 | where: { name: params.thingName }, 54 | }); 55 | 56 | return this.findOrCreateOne({ 57 | where: { 58 | user: { id: params.user.id }, 59 | alias: params.alias, 60 | thing: { id: thing.id }, 61 | }, 62 | }); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/server/app/auth/cognito/cognito-oauth.strategy.ts: -------------------------------------------------------------------------------- 1 | import { PassportStrategy } from '@nestjs/passport'; 2 | import { Strategy } from 'passport-oauth2'; 3 | import { Injectable } from '@nestjs/common'; 4 | import { ConfigService } from '@nestjs/config'; 5 | import axios from 'axios'; 6 | 7 | import { UsersService } from '../../users/users.service'; 8 | 9 | @Injectable() 10 | export class CognitoOauthStrategy extends PassportStrategy( 11 | Strategy, 12 | 'cognito', 13 | ) { 14 | private domain: string; 15 | private region: string; 16 | 17 | constructor( 18 | configService: ConfigService, 19 | private readonly usersService: UsersService, 20 | ) { 21 | super({ 22 | authorizationURL: CognitoOauthStrategy.authorizationUrl( 23 | configService.get('OAUTH_COGNITO_DOMAIN'), 24 | configService.get('OAUTH_COGNITO_REGION'), 25 | ), 26 | tokenURL: CognitoOauthStrategy.tokenUrl( 27 | configService.get('OAUTH_COGNITO_DOMAIN'), 28 | configService.get('OAUTH_COGNITO_REGION'), 29 | ), 30 | clientID: configService.get('OAUTH_COGNITO_ID'), 31 | clientSecret: configService.get('OAUTH_COGNITO_SECRET'), 32 | callbackURL: configService.get('OAUTH_COGNITO_REDIRECT_URL'), 33 | }); 34 | this.domain = configService.get('OAUTH_COGNITO_DOMAIN'); 35 | this.region = configService.get('OAUTH_COGNITO_REGION'); 36 | } 37 | 38 | static baseUrl(domain: string, region: string): string { 39 | return `https://${domain}.auth.${region}.amazoncognito.com/oauth2`; 40 | } 41 | 42 | static authorizationUrl(domain: string, region: string): string { 43 | return `${this.baseUrl(domain, region)}/authorize`; 44 | } 45 | 46 | static tokenUrl(domain: string, region: string): string { 47 | return `${this.baseUrl(domain, region)}/token`; 48 | } 49 | 50 | static userInfoUrl(domain: string, region: string): string { 51 | return `${this.baseUrl(domain, region)}/userInfo`; 52 | } 53 | 54 | async validate(accessToken: string) { 55 | // Here the `id_token` is also received: https://docs.aws.amazon.com/cognito/latest/developerguide/token-endpoint.html 56 | // But it's not supported by passport-oauth2, only `access_token` is received 57 | // Therefore another call is made to the userinfo endpoint 58 | const userinfo = ( 59 | await axios.get( 60 | CognitoOauthStrategy.userInfoUrl(this.domain, this.region), 61 | { headers: { Authorization: `Bearer ${accessToken}` } }, 62 | ) 63 | ).data; 64 | 65 | let [provider, providerId] = userinfo.username.split('_'); 66 | if (!providerId) { 67 | provider = 'cognito'; 68 | providerId = userinfo.username; 69 | } 70 | 71 | let user = await this.usersService.findOne({ 72 | where: { provider, providerId }, 73 | }); 74 | if (!user) { 75 | user = await this.usersService.create({ 76 | provider, 77 | providerId, 78 | name: userinfo.name, 79 | username: userinfo.email, 80 | }); 81 | } 82 | 83 | return user; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "next-nest", 3 | "private": true, 4 | "version": "1.0.0", 5 | "description": "Nest TypeScript starter repository", 6 | "license": "MIT", 7 | "scripts": { 8 | "nest": "nest", 9 | "next": "cd src/client && next", 10 | "prebuild": "rimraf dist", 11 | "build": "NODE_ENV=production nest build && cd src/client && next build", 12 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", 13 | "start": "nest start", 14 | "start:dev": "nest start --watch --preserveWatchOutput", 15 | "start:debug": "nest start --debug --watch --preserveWatchOutput", 16 | "start:prod": "NODE_ENV=production node dist/src/server/main", 17 | "lint": "eslint \"{src,test}/**/*.{ts,tsx}\"", 18 | "test": "jest --setupFiles dotenv-flow/config --collect-coverage", 19 | "test:watch": "jest --watch", 20 | "test:cov": "jest --coverage", 21 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 22 | "test:request": "jest --setupFiles dotenv-flow/config --config ./test/jest-request.json", 23 | "console": "ts-node -r tsconfig-paths/register src/server/console.ts", 24 | "typeorm": "typeorm-ts-node-commonjs -d ormconfig.ts", 25 | "client:generate:schema": "zeus src/schema.gql src/client/app/types --typescript --apollo", 26 | "cy": "cypress" 27 | }, 28 | "dependencies": { 29 | "@apollo/client": "^3.7.17", 30 | "@apollo/gateway": "^2.4.12", 31 | "@babel/runtime": "^7.20.7", 32 | "@nestjs/apollo": "^10.1.7", 33 | "@nestjs/common": "^9.1.6", 34 | "@nestjs/config": "^2.2.0", 35 | "@nestjs/core": "^9.1.6", 36 | "@nestjs/graphql": "^10.1.7", 37 | "@nestjs/jwt": "^10.0.1", 38 | "@nestjs/passport": "^9.0.0", 39 | "@nestjs/platform-express": "^9.2.1", 40 | "@nestjs/typeorm": "^10.0.0", 41 | "apollo-server-express": "^3.11.1", 42 | "axios": "^1.2.2", 43 | "commander": "^11.0.0", 44 | "cookie-parser": "^1.4.6", 45 | "google-auth-library": "^8.7.0", 46 | "graphql": "^16.7.1", 47 | "nestjs-console": "^8.0.0", 48 | "next": "^13.1.0", 49 | "passport": "^0.6.0", 50 | "passport-google-oauth20": "^2.0.0", 51 | "passport-jwt": "^4.0.1", 52 | "passport-local": "^1.0.0", 53 | "passport-oauth2": "^1.6.1", 54 | "pg": "^8.11.2", 55 | "react": "^18.2.0", 56 | "react-dom": "^18.2.0", 57 | "reflect-metadata": "^0.1.13", 58 | "rimraf": "^3.0.2", 59 | "rxjs": "^7.8.0", 60 | "ts-morph": "^17.0.1", 61 | "typeorm": "^0.3.11" 62 | }, 63 | "devDependencies": { 64 | "@faker-js/faker": "^7.6.0", 65 | "@nestjs/cli": "^10.1.10", 66 | "@nestjs/schematics": "^10.0.2", 67 | "@nestjs/testing": "^9.2.1", 68 | "@next/eslint-plugin-next": "^13.1.1", 69 | "@types/cookie-parser": "^1.4.3", 70 | "@types/express": "^4.17.17", 71 | "@types/gtag.js": "^0.0.12", 72 | "@types/jest": "^28.1.8", 73 | "@types/node": "^18.11.18", 74 | "@types/passport-google-oauth20": "^2.0.11", 75 | "@types/passport-jwt": "^3.0.8", 76 | "@types/passport-local": "^1.0.34", 77 | "@types/passport-oauth2": "^1.4.11", 78 | "@types/react": "^18.0.26", 79 | "@types/react-dom": "^18.0.10", 80 | "@types/supertest": "^2.0.12", 81 | "@typescript-eslint/eslint-plugin": "^5.47.1", 82 | "@typescript-eslint/parser": "^5.47.1", 83 | "cypress": "^12.2.0", 84 | "dotenv-flow": "^3.2.0", 85 | "eslint": "^8.31.0", 86 | "eslint-config-prettier": "^8.8.0", 87 | "eslint-plugin-prettier": "^4.2.1", 88 | "fishery": "^2.2.2", 89 | "graphql-zeus": "^5.3.2", 90 | "jest": "^28.1.3", 91 | "prettier": "^2.8.1", 92 | "supertest": "^6.3.3", 93 | "ts-jest": "^28.0.8", 94 | "ts-loader": "^9.4.2", 95 | "ts-node": "^10.9.1", 96 | "tsconfig-paths": "^4.1.1", 97 | "typescript": "^4.9.4" 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /test/app.request-spec.ts: -------------------------------------------------------------------------------- 1 | import * as request from 'supertest'; 2 | import { Test } from '@nestjs/testing'; 3 | import { AppModule } from 'src/server/app/app.module'; 4 | import { INestApplication } from '@nestjs/common'; 5 | import { DataSource } from 'typeorm'; 6 | import * as cookieParser from 'cookie-parser'; 7 | 8 | import { usersFactory, ordersFactory, thingsFactory } from 'test/factories'; 9 | import { UsersService } from 'src/server/app/users/users.service'; 10 | import { User } from 'src/server/app/users/user.entity'; 11 | import { Order } from 'src/server/app/orders/order.entity'; 12 | import { OrdersService } from 'src/server/app/orders/orders.service'; 13 | import { JwtAuthService } from 'src/server/app/auth/jwt/jwt-auth.service'; 14 | import { ThingsService } from 'src/server/app/things/things.service'; 15 | import { login } from './utils'; 16 | 17 | describe('Application', () => { 18 | let app: INestApplication; 19 | let authService: JwtAuthService; 20 | let usersService: UsersService; 21 | let ordersService: OrdersService; 22 | let thingsService: ThingsService; 23 | let dataSource: DataSource; 24 | 25 | beforeAll(async () => { 26 | const moduleFixture = await Test.createTestingModule({ 27 | imports: [AppModule], 28 | }).compile(); 29 | 30 | app = moduleFixture.createNestApplication(); 31 | app.use(cookieParser()); 32 | await app.init(); 33 | 34 | usersService = app.get(UsersService); 35 | ordersService = app.get(OrdersService); 36 | authService = app.get(JwtAuthService); 37 | thingsService = app.get(ThingsService); 38 | dataSource = app.get(DataSource); 39 | }); 40 | 41 | beforeEach(async () => { 42 | await dataSource.synchronize(true); 43 | }); 44 | 45 | describe('GraphQL', () => { 46 | const endpoint = '/graphql'; 47 | let agent: request.Test; 48 | 49 | beforeEach(async () => { 50 | agent = request(app.getHttpServer()).post(endpoint); 51 | }); 52 | 53 | describe('users', () => { 54 | const query = '{ users { id provider } }'; 55 | let user: User; 56 | 57 | beforeEach(async () => { 58 | user = await usersService.create(usersFactory.build()); 59 | }); 60 | 61 | it('returns users', () => { 62 | return agent 63 | .send({ query: query }) 64 | .expect(200) 65 | .expect((res) => { 66 | expect(res.body.data.users).toHaveLength(1); 67 | expect(res.body.data.users[0].id).toEqual(user.id); 68 | expect(res.body.data.users[0].provider).toEqual(user.provider); 69 | }); 70 | }); 71 | }); 72 | 73 | describe('orders', () => { 74 | const query = '{ orders { id alias } }'; 75 | let user: User; 76 | let order: Order; 77 | 78 | it('returns unauthorized', () => { 79 | return agent 80 | .send({ query: query }) 81 | .expect(200) 82 | .expect((res) => { 83 | expect(res.body.errors).toHaveLength(1); 84 | expect(res.body.errors[0].message).toEqual('Unauthorized'); 85 | }); 86 | }); 87 | 88 | describe('when authorized', () => { 89 | beforeEach(async () => { 90 | user = await usersService.create(usersFactory.build()); 91 | const thing = await thingsService.create(thingsFactory.build()); 92 | order = await ordersService.create( 93 | ordersFactory.build( 94 | {}, 95 | { associations: { user: user, thing: thing } }, 96 | ), 97 | ); 98 | }); 99 | 100 | it('returns orders', () => { 101 | return login(agent, user, authService) 102 | .send({ query: query }) 103 | .expect(200) 104 | .expect((res) => { 105 | expect(res.body.data.orders).toHaveLength(1); 106 | expect(res.body.data.orders[0].id).toEqual(order.id); 107 | }); 108 | }); 109 | }); 110 | }); 111 | }); 112 | }); 113 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # next-nest 2 | [![CI](https://github.com/thisismydesign/next-nest/actions/workflows/ci.yml/badge.svg)](https://github.com/thisismydesign/next-nest/actions/workflows/ci.yml) 3 | 4 | #### Next.js + NestJS MVC monolith for rapid development with battle-tested standards. 5 | 6 | - Build a web app using the most popular JavaScript backend and frontend frameworks. 7 | - Get started in minutes with a unified codebase - no need to sync or deploy multiple services. 8 | - Use typed DB data directly in React via GraphQL and SSR. 9 | - End-to-end type safety from database to forms. 10 | - When the time is right, you can easily split the frontend and backend into separate services. 11 | 12 | [Use this template](https://github.com/thisismydesign/next-nest/generate) 13 | 14 | Featured in: 15 | - [Geek Culture: NestJS + React (Next.js) in One MVC Repo for Rapid Prototyping](https://medium.com/geekculture/nestjs-react-next-js-in-one-mvc-repo-for-rapid-prototyping-faed42a194ca) 16 | - [Geek Culture: Automagically Typed GraphQL Queries and Results With Apollo](https://medium.com/geekculture/automagically-typed-graphql-queries-and-results-with-apollo-3731bad989aa) 17 | - [JavaScript in Plain English: OAuth2 in NestJS for Social Login (Google, Facebook, Twitter, etc)](https://javascript.plainenglish.io/oauth2-in-nestjs-for-social-login-google-facebook-twitter-etc-8b405d570fd2) 18 | - [JavaScript in Plain English: Cognito via OAuth2 in NestJS: Outsourcing Authentication Without Vendor Lock-in](https://javascript.plainenglish.io/cognito-via-oauth2-in-nestjs-outsourcing-authentication-without-vendor-lock-in-ce908518f547) 19 | 20 | ## Stack 21 | 22 | It has 23 | - Example REST and GraphQL modules, DB using TypeORM as seen on https://docs.nestjs.com/ 24 | - [Next.js](https://nextjs.org/) integration for React on the frontend ([howto article](https://csaba-apagyi.medium.com/nestjs-react-next-js-in-one-mvc-repo-for-rapid-prototyping-faed42a194ca)) 25 | - Typed queries & results with GraphQL out of the box ([howto article](https://csaba-apagyi.medium.com/automagically-typed-graphql-queries-and-results-with-apollo-3731bad989aa)) 26 | - Authentication via [Passport.js](http://www.passportjs.org/) including Social providers ([howto article](https://medium.com/csaba.apagyi/oauth2-in-nestjs-for-social-login-google-facebook-twitter-etc-8b405d570fd2)), [AWS Cognito](https://aws.amazon.com/cognito/) ([howto article](https://medium.com/csaba.apagyi/cognito-via-oauth2-in-nestjs-outsourcing-authentication-without-vendor-lock-in-ce908518f547)), and JWT strategy for REST and GraphQL 27 | - Docker setup 28 | - Typescript, ESLint 29 | - CI via GitHub Actions 30 | - Running tasks (e.g. DB seeding) via [nestjs-console](https://github.com/Pop-Code/nestjs-console) 31 | - Unit and integration testing via Jest 32 | - Google Analytics 4 33 | 34 | ## Usage 35 | 36 | ### Dev 37 | 38 | ```sh 39 | cp .env.example .env 40 | 41 | docker compose up 42 | 43 | docker compose exec web yarn lint 44 | docker compose exec web yarn test 45 | docker compose exec web yarn test:request 46 | docker compose exec web yarn build 47 | docker run -it -v $PWD:/e2e -w /e2e --network="host" --entrypoint=cypress cypress/included:12.2.0 run 48 | ``` 49 | 50 | ## Functionality 51 | 52 | REST endpoint via Nest 53 | - http://localhost:3000/ 54 | 55 | JWT-protected REST endpoint via Nest 56 | - http://localhost:3000/private 57 | 58 | GraphQL playground (`query WhoAmI` is JWT-protected) 59 | - http://localhost:3000/graphql 60 | ```qgl 61 | query Public { 62 | things { 63 | id 64 | name 65 | } 66 | 67 | users { 68 | id 69 | provider 70 | } 71 | } 72 | 73 | # Add Header: { "Authorization": "Bearer " } 74 | query Private { 75 | whoAmI { 76 | id, 77 | provider, 78 | providerId, 79 | username, 80 | name 81 | } 82 | 83 | orders { 84 | id 85 | 86 | alias 87 | thing { 88 | name 89 | } 90 | } 91 | } 92 | 93 | mutation createOrder { 94 | createOrder(alias: "myname", thingName: "this is a thing you can order") { 95 | id 96 | alias 97 | } 98 | } 99 | ``` 100 | 101 | Cognito auth (redirects to hosted Cognito UI) 102 | - http://localhost:3000/auth/cognito 103 | 104 | Google auth 105 | - http://localhost:3000/auth/google 106 | 107 | Next.js page 108 | - http://localhost:3000/home 109 | 110 | JWT-protected Next.js page 111 | - http://localhost:3000/profile 112 | -------------------------------------------------------------------------------- /src/server/app/orders/orders.resolver.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { TypeOrmModule } from '@nestjs/typeorm'; 3 | import { UsersModule } from '../users/users.module'; 4 | import { UsersService } from '../users/users.service'; 5 | import { OrdersModule } from './orders.module'; 6 | import { OrdersResolver } from './orders.resolver'; 7 | import { OrdersService } from './orders.service'; 8 | import { usersFactory, thingsFactory, ordersFactory } from 'test/factories'; 9 | import { ThingsModule } from '../things/things.module'; 10 | import { ThingsService } from '../things/things.service'; 11 | import { ConfigModule, ConfigService } from '@nestjs/config'; 12 | 13 | describe('OrdersResolver', () => { 14 | let resolver: OrdersResolver; 15 | let ordersService: OrdersService; 16 | let usersService: UsersService; 17 | let thingsService: ThingsService; 18 | let moduleRef: TestingModule; 19 | 20 | beforeEach(async () => { 21 | moduleRef = await Test.createTestingModule({ 22 | imports: [ 23 | ConfigModule.forRoot({ 24 | isGlobal: true, 25 | }), 26 | TypeOrmModule.forRootAsync({ 27 | useFactory: async (configService: ConfigService) => ({ 28 | type: 'postgres', 29 | url: configService.get('DATABASE_URL'), 30 | autoLoadEntities: true, 31 | synchronize: true, 32 | dropSchema: true, 33 | }), 34 | inject: [ConfigService], 35 | }), 36 | OrdersModule, 37 | UsersModule, 38 | ThingsModule, 39 | ], 40 | }).compile(); 41 | 42 | resolver = moduleRef.get(OrdersResolver); 43 | ordersService = moduleRef.get(OrdersService); 44 | usersService = moduleRef.get(UsersService); 45 | thingsService = moduleRef.get(ThingsService); 46 | }); 47 | 48 | afterEach(async () => { 49 | await moduleRef.close(); 50 | }); 51 | 52 | describe('orders', () => { 53 | it('returns orders of user', async () => { 54 | const user = await usersService.create(usersFactory.build()); 55 | const thing = await thingsService.create(thingsFactory.build()); 56 | const order = await ordersService.create( 57 | ordersFactory.build({}, { associations: { user: user, thing: thing } }), 58 | ); 59 | 60 | const result = await resolver.orders(user); 61 | 62 | expect([order]).toMatchObject(result); 63 | }); 64 | 65 | it('does not return orders of another user', async () => { 66 | const anotherUser = await usersService.create(usersFactory.build()); 67 | const thing = await thingsService.create(thingsFactory.build()); 68 | await ordersService.create( 69 | ordersFactory.build( 70 | {}, 71 | { associations: { user: anotherUser, thing: thing } }, 72 | ), 73 | ); 74 | 75 | const user = await usersService.create(usersFactory.build()); 76 | const result = await resolver.orders(user); 77 | 78 | expect(result).toEqual([]); 79 | }); 80 | }); 81 | 82 | describe('createOrder', () => { 83 | it('returns the order', async () => { 84 | const user = await usersService.create(usersFactory.build()); 85 | const thing = await thingsService.create(thingsFactory.build()); 86 | const alias = ordersFactory.build().alias; 87 | 88 | const result = await resolver.createOrder(user, thing.name, alias); 89 | 90 | expect(result).toMatchObject({ alias: alias }); 91 | }); 92 | 93 | it('creates an order', async () => { 94 | const user = await usersService.create(usersFactory.build()); 95 | const thing = await thingsService.create(thingsFactory.build()); 96 | const alias = ordersFactory.build().alias; 97 | 98 | await resolver.createOrder(user, thing.name, alias); 99 | 100 | const orderCount = ( 101 | await ordersService.findAll({ where: { user: { id: user.id } } }) 102 | ).length; 103 | expect(orderCount).toEqual(1); 104 | }); 105 | 106 | it('does not create the same order twice', async () => { 107 | const user = await usersService.create(usersFactory.build()); 108 | const thing = await thingsService.create(thingsFactory.build()); 109 | const alias = ordersFactory.build().alias; 110 | 111 | await resolver.createOrder(user, thing.name, alias); 112 | await resolver.createOrder(user, thing.name, alias); 113 | 114 | const orderCount = ( 115 | await ordersService.findAll({ where: { user: { id: user.id } } }) 116 | ).length; 117 | expect(orderCount).toEqual(1); 118 | }); 119 | }); 120 | }); 121 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | pull_request: 6 | types: [opened, reopened] 7 | 8 | jobs: 9 | verify-docker-setup: 10 | runs-on: ubuntu-latest 11 | timeout-minutes: 10 12 | 13 | steps: 14 | - uses: actions/checkout@v3 15 | 16 | - name: Init 17 | run: ./scripts/docker_setup_init.sh 18 | 19 | - name: Pull images 20 | run: docker compose pull db 21 | 22 | - name: Docker build 23 | run: docker compose build web 24 | 25 | - name: Docker init 26 | run: docker compose run --rm web sh -c 'exit 0' 27 | 28 | build-docker-image: 29 | runs-on: ubuntu-latest 30 | timeout-minutes: 10 31 | outputs: 32 | docker-tag: ${{ steps.meta.outputs.tags }} 33 | 34 | steps: 35 | - uses: actions/checkout@v3 36 | 37 | - name: Set up Docker Buildx to be able to use caching 38 | uses: docker/setup-buildx-action@v2 39 | 40 | - name: Extract metadata (tags, labels) for Docker 41 | id: meta 42 | uses: docker/metadata-action@v3 43 | with: 44 | images: local/${{ github.repository }} 45 | 46 | - name: Build image 47 | uses: docker/build-push-action@v3 48 | with: 49 | file: Dockerfile.prod 50 | tags: ${{ steps.meta.outputs.tags }} 51 | cache-from: type=gha 52 | cache-to: type=gha 53 | 54 | lint: 55 | runs-on: ubuntu-latest 56 | timeout-minutes: 10 57 | needs: [build-docker-image] 58 | env: 59 | NESTJS_STARTER_IMAGE: ${{ needs.build-docker-image.outputs.docker-tag }} 60 | 61 | steps: 62 | - uses: actions/checkout@v3 63 | 64 | - name: Set up Docker Buildx to be able to use caching 65 | uses: docker/setup-buildx-action@v2 66 | 67 | - name: Build image 68 | uses: docker/build-push-action@v3 69 | with: 70 | load: true 71 | file: Dockerfile.prod 72 | tags: ${{ needs.build-docker-image.outputs.docker-tag }} 73 | cache-from: type=gha 74 | cache-to: type=gha 75 | 76 | - name: Init 77 | run: ./scripts/docker_setup_init.sh 78 | 79 | - name: Pull images 80 | run: docker compose pull db 81 | 82 | - name: Run lint 83 | run: docker compose run web yarn lint 84 | 85 | test: 86 | runs-on: ubuntu-latest 87 | timeout-minutes: 10 88 | needs: [build-docker-image] 89 | env: 90 | NESTJS_STARTER_IMAGE: ${{ needs.build-docker-image.outputs.docker-tag }} 91 | 92 | steps: 93 | - uses: actions/checkout@v3 94 | 95 | - name: Set up Docker Buildx to be able to use caching 96 | uses: docker/setup-buildx-action@v2 97 | 98 | - name: Build image 99 | uses: docker/build-push-action@v3 100 | with: 101 | load: true 102 | file: Dockerfile.prod 103 | tags: ${{ needs.build-docker-image.outputs.docker-tag }} 104 | cache-from: type=gha 105 | cache-to: type=gha 106 | 107 | - name: Init 108 | run: ./scripts/docker_setup_init.sh 109 | 110 | - name: Pull images 111 | run: docker compose pull db 112 | 113 | - name: Run tests 114 | run: docker compose run web yarn test 115 | 116 | test-request: 117 | runs-on: ubuntu-latest 118 | timeout-minutes: 10 119 | needs: [build-docker-image] 120 | env: 121 | NESTJS_STARTER_IMAGE: ${{ needs.build-docker-image.outputs.docker-tag }} 122 | 123 | steps: 124 | - uses: actions/checkout@v3 125 | 126 | - name: Set up Docker Buildx to be able to use caching 127 | uses: docker/setup-buildx-action@v2 128 | 129 | - name: Build image 130 | uses: docker/build-push-action@v3 131 | with: 132 | load: true 133 | file: Dockerfile.prod 134 | tags: ${{ needs.build-docker-image.outputs.docker-tag }} 135 | cache-from: type=gha 136 | cache-to: type=gha 137 | 138 | - name: Init 139 | run: ./scripts/docker_setup_init.sh 140 | 141 | - name: Pull images 142 | run: docker compose pull db 143 | 144 | - name: Run request tests 145 | run: docker compose run web yarn test:request 146 | 147 | test-e2e: 148 | runs-on: ubuntu-latest 149 | timeout-minutes: 10 150 | needs: [build-docker-image] 151 | env: 152 | NESTJS_STARTER_IMAGE: ${{ needs.build-docker-image.outputs.docker-tag }} 153 | DUMMY_MOUNT: .:/dummy 154 | 155 | steps: 156 | - uses: actions/checkout@v3 157 | 158 | - name: Set up Docker Buildx to be able to use caching 159 | uses: docker/setup-buildx-action@v2 160 | 161 | - name: Build image 162 | uses: docker/build-push-action@v3 163 | with: 164 | load: true 165 | file: Dockerfile.prod 166 | tags: ${{ needs.build-docker-image.outputs.docker-tag }} 167 | cache-from: type=gha 168 | cache-to: type=gha 169 | 170 | - name: Init 171 | run: ./scripts/docker_setup_init.sh 172 | 173 | - name: Pull images 174 | run: docker compose pull db 175 | 176 | - name: Docker up 177 | run: docker compose up --detach 178 | 179 | - name: Cypress run 180 | uses: cypress-io/github-action@v4 181 | with: 182 | wait-on: 'http://localhost:3000' 183 | wait-on-timeout: 120 184 | 185 | - name: Save Cypress artifacts 186 | uses: actions/upload-artifact@v4 187 | if: always() 188 | with: 189 | name: cypress-artifacts 190 | path: | 191 | cypress/videos 192 | cypress/screenshots 193 | 194 | - name: Docker setup logs 195 | if: always() 196 | run: docker compose logs --timestamps --tail="all" > docker-logs.txt 197 | 198 | - name: Save log artifacts 199 | uses: actions/upload-artifact@v4 200 | if: always() 201 | with: 202 | name: log-artifacts 203 | path: docker-logs.txt 204 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### JetBrains template 3 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 4 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 5 | 6 | # User-specific stuff: 7 | .idea/**/workspace.xml 8 | .idea/**/tasks.xml 9 | .idea/dictionaries 10 | 11 | # Sensitive or high-churn files: 12 | .idea/**/dataSources/ 13 | .idea/**/dataSources.ids 14 | .idea/**/dataSources.xml 15 | .idea/**/dataSources.local.xml 16 | .idea/**/sqlDataSources.xml 17 | .idea/**/dynamic.xml 18 | .idea/**/uiDesigner.xml 19 | 20 | # Gradle: 21 | .idea/**/gradle.xml 22 | .idea/**/libraries 23 | 24 | # CMake 25 | cmake-build-debug/ 26 | 27 | # Mongo Explorer plugin: 28 | .idea/**/mongoSettings.xml 29 | 30 | ## File-based project format: 31 | *.iws 32 | 33 | ## Plugin-specific files: 34 | 35 | # IntelliJ 36 | out/ 37 | 38 | # mpeltonen/sbt-idea plugin 39 | .idea_modules/ 40 | 41 | # JIRA plugin 42 | atlassian-ide-plugin.xml 43 | 44 | # Cursive Clojure plugin 45 | .idea/replstate.xml 46 | 47 | # Crashlytics plugin (for Android Studio and IntelliJ) 48 | com_crashlytics_export_strings.xml 49 | crashlytics.properties 50 | crashlytics-build.properties 51 | fabric.properties 52 | ### VisualStudio template 53 | ## Ignore Visual Studio temporary files, build results, and 54 | ## files generated by popular Visual Studio add-ons. 55 | ## 56 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 57 | 58 | # User-specific files 59 | *.suo 60 | *.user 61 | *.userosscache 62 | *.sln.docstates 63 | 64 | # User-specific files (MonoDevelop/Xamarin Studio) 65 | *.userprefs 66 | 67 | # Build results 68 | [Dd]ebug/ 69 | [Dd]ebugPublic/ 70 | [Rr]elease/ 71 | [Rr]eleases/ 72 | x64/ 73 | x86/ 74 | bld/ 75 | [Bb]in/ 76 | [Oo]bj/ 77 | [Ll]og/ 78 | 79 | # Visual Studio 2015 cache/options directory 80 | .vs/ 81 | # Uncomment if you have tasks that create the project's static files in wwwroot 82 | #wwwroot/ 83 | 84 | # MSTest test Results 85 | [Tt]est[Rr]esult*/ 86 | [Bb]uild[Ll]og.* 87 | 88 | # NUNIT 89 | *.VisualState.xml 90 | TestResult.xml 91 | 92 | # Build Results of an ATL Project 93 | [Dd]ebugPS/ 94 | [Rr]eleasePS/ 95 | dlldata.c 96 | 97 | # Benchmark Results 98 | BenchmarkDotNet.Artifacts/ 99 | 100 | # .NET Core 101 | project.lock.json 102 | project.fragment.lock.json 103 | artifacts/ 104 | **/Properties/launchSettings.json 105 | 106 | *_i.c 107 | *_p.c 108 | *_i.h 109 | *.ilk 110 | *.meta 111 | *.obj 112 | *.pch 113 | *.pdb 114 | *.pgc 115 | *.pgd 116 | *.rsp 117 | *.sbr 118 | *.tlb 119 | *.tli 120 | *.tlh 121 | *.tmp 122 | *.tmp_proj 123 | *.log 124 | *.vspscc 125 | *.vssscc 126 | .builds 127 | *.pidb 128 | *.svclog 129 | *.scc 130 | 131 | # Chutzpah Test files 132 | _Chutzpah* 133 | 134 | # Visual C++ cache files 135 | ipch/ 136 | *.aps 137 | *.ncb 138 | *.opendb 139 | *.opensdf 140 | *.sdf 141 | *.cachefile 142 | *.VC.db 143 | *.VC.VC.opendb 144 | 145 | # Visual Studio profiler 146 | *.psess 147 | *.vsp 148 | *.vspx 149 | *.sap 150 | 151 | # Visual Studio Trace Files 152 | *.e2e 153 | 154 | # TFS 2012 Local Workspace 155 | $tf/ 156 | 157 | # Guidance Automation Toolkit 158 | *.gpState 159 | 160 | # ReSharper is a .NET coding add-in 161 | _ReSharper*/ 162 | *.[Rr]e[Ss]harper 163 | *.DotSettings.user 164 | 165 | # JustCode is a .NET coding add-in 166 | .JustCode 167 | 168 | # TeamCity is a build add-in 169 | _TeamCity* 170 | 171 | # DotCover is a Code Coverage Tool 172 | *.dotCover 173 | 174 | # AxoCover is a Code Coverage Tool 175 | .axoCover/* 176 | !.axoCover/settings.json 177 | 178 | # Visual Studio code coverage results 179 | *.coverage 180 | *.coveragexml 181 | 182 | # NCrunch 183 | _NCrunch_* 184 | .*crunch*.local.xml 185 | nCrunchTemp_* 186 | 187 | # MightyMoose 188 | *.mm.* 189 | AutoTest.Net/ 190 | 191 | # Web workbench (sass) 192 | .sass-cache/ 193 | 194 | # Installshield output folder 195 | [Ee]xpress/ 196 | 197 | # DocProject is a documentation generator add-in 198 | DocProject/buildhelp/ 199 | DocProject/Help/*.HxT 200 | DocProject/Help/*.HxC 201 | DocProject/Help/*.hhc 202 | DocProject/Help/*.hhk 203 | DocProject/Help/*.hhp 204 | DocProject/Help/Html2 205 | DocProject/Help/html 206 | 207 | # Click-Once directory 208 | publish/ 209 | 210 | # Publish Web Output 211 | *.[Pp]ublish.xml 212 | *.azurePubxml 213 | # Note: Comment the next line if you want to checkin your web deploy settings, 214 | # but database connection strings (with potential passwords) will be unencrypted 215 | *.pubxml 216 | *.publishproj 217 | 218 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 219 | # checkin your Azure Web App publish settings, but sensitive information contained 220 | # in these scripts will be unencrypted 221 | PublishScripts/ 222 | 223 | # NuGet Packages 224 | *.nupkg 225 | # The packages folder can be ignored because of Package Restore 226 | **/[Pp]ackages/* 227 | # except build/, which is used as an MSBuild target. 228 | !**/[Pp]ackages/build/ 229 | # Uncomment if necessary however generally it will be regenerated when needed 230 | #!**/[Pp]ackages/repositories.config 231 | # NuGet v3's project.json files produces more ignorable files 232 | *.nuget.props 233 | *.nuget.targets 234 | 235 | # Microsoft Azure Build Output 236 | csx/ 237 | *.build.csdef 238 | 239 | # Microsoft Azure Emulator 240 | ecf/ 241 | rcf/ 242 | 243 | # Windows Store app package directories and files 244 | AppPackages/ 245 | BundleArtifacts/ 246 | Package.StoreAssociation.xml 247 | _pkginfo.txt 248 | *.appx 249 | 250 | # Visual Studio cache files 251 | # files ending in .cache can be ignored 252 | *.[Cc]ache 253 | # but keep track of directories ending in .cache 254 | !*.[Cc]ache/ 255 | 256 | # Others 257 | ClientBin/ 258 | ~$* 259 | *~ 260 | *.dbmdl 261 | *.dbproj.schemaview 262 | *.jfm 263 | *.pfx 264 | *.publishsettings 265 | orleans.codegen.cs 266 | 267 | # Since there are multiple workflows, uncomment next line to ignore bower_components 268 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 269 | #bower_components/ 270 | 271 | # RIA/Silverlight projects 272 | Generated_Code/ 273 | 274 | # Backup & report files from converting an old project file 275 | # to a newer Visual Studio version. Backup files are not needed, 276 | # because we have git ;-) 277 | _UpgradeReport_Files/ 278 | Backup*/ 279 | UpgradeLog*.XML 280 | UpgradeLog*.htm 281 | 282 | # SQL Server files 283 | *.mdf 284 | *.ldf 285 | *.ndf 286 | 287 | # Business Intelligence projects 288 | *.rdl.data 289 | *.bim.layout 290 | *.bim_*.settings 291 | 292 | # Microsoft Fakes 293 | FakesAssemblies/ 294 | 295 | # GhostDoc plugin setting file 296 | *.GhostDoc.xml 297 | 298 | # Node.js Tools for Visual Studio 299 | .ntvs_analysis.dat 300 | node_modules/ 301 | 302 | # Typescript v1 declaration files 303 | typings/ 304 | 305 | # Visual Studio 6 build log 306 | *.plg 307 | 308 | # Visual Studio 6 workspace options file 309 | *.opt 310 | 311 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 312 | *.vbw 313 | 314 | # Visual Studio LightSwitch build output 315 | **/*.HTMLClient/GeneratedArtifacts 316 | **/*.DesktopClient/GeneratedArtifacts 317 | **/*.DesktopClient/ModelManifest.xml 318 | **/*.Server/GeneratedArtifacts 319 | **/*.Server/ModelManifest.xml 320 | _Pvt_Extensions 321 | 322 | # Paket dependency manager 323 | .paket/paket.exe 324 | paket-files/ 325 | 326 | # FAKE - F# Make 327 | .fake/ 328 | 329 | # JetBrains Rider 330 | .idea/ 331 | *.sln.iml 332 | 333 | # IDE - VSCode 334 | .vscode/* 335 | !.vscode/settings.json 336 | !.vscode/tasks.json 337 | !.vscode/launch.json 338 | !.vscode/extensions.json 339 | 340 | # CodeRush 341 | .cr/ 342 | 343 | # Python Tools for Visual Studio (PTVS) 344 | __pycache__/ 345 | *.pyc 346 | 347 | # Cake - Uncomment if you are using it 348 | # tools/** 349 | # !tools/packages.config 350 | 351 | # Tabs Studio 352 | *.tss 353 | 354 | # Telerik's JustMock configuration file 355 | *.jmconfig 356 | 357 | # BizTalk build output 358 | *.btp.cs 359 | *.btm.cs 360 | *.odx.cs 361 | *.xsd.cs 362 | 363 | # OpenCover UI analysis results 364 | OpenCover/ 365 | coverage/ 366 | 367 | ### macOS template 368 | # General 369 | .DS_Store 370 | .AppleDouble 371 | .LSOverride 372 | 373 | # Icon must end with two \r 374 | Icon 375 | 376 | # Thumbnails 377 | ._* 378 | 379 | # Files that might appear in the root of a volume 380 | .DocumentRevisions-V100 381 | .fseventsd 382 | .Spotlight-V100 383 | .TemporaryItems 384 | .Trashes 385 | .VolumeIcon.icns 386 | .com.apple.timemachine.donotpresent 387 | 388 | # Directories potentially created on remote AFP share 389 | .AppleDB 390 | .AppleDesktop 391 | Network Trash Folder 392 | Temporary Items 393 | .apdisk 394 | 395 | ======= 396 | # Local 397 | .env 398 | dist 399 | 400 | notes.md 401 | 402 | .next 403 | 404 | cypress/videos 405 | cypress/screenshots 406 | -------------------------------------------------------------------------------- /src/client/app/types/zeus/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | import { AllTypesProps, ReturnTypes, Ops } from './const'; 4 | 5 | 6 | export const HOST="Specify host" 7 | 8 | 9 | export const HEADERS = {} 10 | export const apiSubscription = (options: chainOptions) => (query: string) => { 11 | try { 12 | const queryString = options[0] + '?query=' + encodeURIComponent(query); 13 | const wsString = queryString.replace('http', 'ws'); 14 | const host = (options.length > 1 && options[1]?.websocket?.[0]) || wsString; 15 | const webSocketOptions = options[1]?.websocket || [host]; 16 | const ws = new WebSocket(...webSocketOptions); 17 | return { 18 | ws, 19 | on: (e: (args: any) => void) => { 20 | ws.onmessage = (event: any) => { 21 | if (event.data) { 22 | const parsed = JSON.parse(event.data); 23 | const data = parsed.data; 24 | return e(data); 25 | } 26 | }; 27 | }, 28 | off: (e: (args: any) => void) => { 29 | ws.onclose = e; 30 | }, 31 | error: (e: (args: any) => void) => { 32 | ws.onerror = e; 33 | }, 34 | open: (e: () => void) => { 35 | ws.onopen = e; 36 | }, 37 | }; 38 | } catch { 39 | throw new Error('No websockets implemented'); 40 | } 41 | }; 42 | const handleFetchResponse = (response: Response): Promise => { 43 | if (!response.ok) { 44 | return new Promise((_, reject) => { 45 | response 46 | .text() 47 | .then((text) => { 48 | try { 49 | reject(JSON.parse(text)); 50 | } catch (err) { 51 | reject(text); 52 | } 53 | }) 54 | .catch(reject); 55 | }); 56 | } 57 | return response.json() as Promise; 58 | }; 59 | 60 | export const apiFetch = 61 | (options: fetchOptions) => 62 | (query: string, variables: Record = {}) => { 63 | const fetchOptions = options[1] || {}; 64 | if (fetchOptions.method && fetchOptions.method === 'GET') { 65 | return fetch(`${options[0]}?query=${encodeURIComponent(query)}`, fetchOptions) 66 | .then(handleFetchResponse) 67 | .then((response: GraphQLResponse) => { 68 | if (response.errors) { 69 | throw new GraphQLError(response); 70 | } 71 | return response.data; 72 | }); 73 | } 74 | return fetch(`${options[0]}`, { 75 | body: JSON.stringify({ query, variables }), 76 | method: 'POST', 77 | headers: { 78 | 'Content-Type': 'application/json', 79 | }, 80 | ...fetchOptions, 81 | }) 82 | .then(handleFetchResponse) 83 | .then((response: GraphQLResponse) => { 84 | if (response.errors) { 85 | throw new GraphQLError(response); 86 | } 87 | return response.data; 88 | }); 89 | }; 90 | 91 | export const InternalsBuildQuery = ({ 92 | ops, 93 | props, 94 | returns, 95 | options, 96 | scalars, 97 | }: { 98 | props: AllTypesPropsType; 99 | returns: ReturnTypesType; 100 | ops: Operations; 101 | options?: OperationOptions; 102 | scalars?: ScalarDefinition; 103 | }) => { 104 | const ibb = ( 105 | k: string, 106 | o: InputValueType | VType, 107 | p = '', 108 | root = true, 109 | vars: Array<{ name: string; graphQLType: string }> = [], 110 | ): string => { 111 | const keyForPath = purifyGraphQLKey(k); 112 | const newPath = [p, keyForPath].join(SEPARATOR); 113 | if (!o) { 114 | return ''; 115 | } 116 | if (typeof o === 'boolean' || typeof o === 'number') { 117 | return k; 118 | } 119 | if (typeof o === 'string') { 120 | return `${k} ${o}`; 121 | } 122 | if (Array.isArray(o)) { 123 | const args = InternalArgsBuilt({ 124 | props, 125 | returns, 126 | ops, 127 | scalars, 128 | vars, 129 | })(o[0], newPath); 130 | return `${ibb(args ? `${k}(${args})` : k, o[1], p, false, vars)}`; 131 | } 132 | if (k === '__alias') { 133 | return Object.entries(o) 134 | .map(([alias, objectUnderAlias]) => { 135 | if (typeof objectUnderAlias !== 'object' || Array.isArray(objectUnderAlias)) { 136 | throw new Error( 137 | 'Invalid alias it should be __alias:{ YOUR_ALIAS_NAME: { OPERATION_NAME: { ...selectors }}}', 138 | ); 139 | } 140 | const operationName = Object.keys(objectUnderAlias)[0]; 141 | const operation = objectUnderAlias[operationName]; 142 | return ibb(`${alias}:${operationName}`, operation, p, false, vars); 143 | }) 144 | .join('\n'); 145 | } 146 | const hasOperationName = root && options?.operationName ? ' ' + options.operationName : ''; 147 | const keyForDirectives = o.__directives ?? ''; 148 | const query = `{${Object.entries(o) 149 | .filter(([k]) => k !== '__directives') 150 | .map((e) => ibb(...e, [p, `field<>${keyForPath}`].join(SEPARATOR), false, vars)) 151 | .join('\n')}}`; 152 | if (!root) { 153 | return `${k} ${keyForDirectives}${hasOperationName} ${query}`; 154 | } 155 | const varsString = vars.map((v) => `${v.name}: ${v.graphQLType}`).join(', '); 156 | return `${k} ${keyForDirectives}${hasOperationName}${varsString ? `(${varsString})` : ''} ${query}`; 157 | }; 158 | return ibb; 159 | }; 160 | 161 | export const Thunder = 162 | (fn: FetchFunction) => 163 | >( 164 | operation: O, 165 | graphqlOptions?: ThunderGraphQLOptions, 166 | ) => 167 | ( 168 | o: (Z & ValueTypes[R]) | ValueTypes[R], 169 | ops?: OperationOptions & { variables?: Record }, 170 | ) => 171 | fn( 172 | Zeus(operation, o, { 173 | operationOptions: ops, 174 | scalars: graphqlOptions?.scalars, 175 | }), 176 | ops?.variables, 177 | ).then((data) => { 178 | if (graphqlOptions?.scalars) { 179 | return decodeScalarsInResponse({ 180 | response: data, 181 | initialOp: operation, 182 | initialZeusQuery: o as VType, 183 | returns: ReturnTypes, 184 | scalars: graphqlOptions.scalars, 185 | ops: Ops, 186 | }); 187 | } 188 | return data; 189 | }) as Promise>; 190 | 191 | export const Chain = (...options: chainOptions) => Thunder(apiFetch(options)); 192 | 193 | export const SubscriptionThunder = 194 | (fn: SubscriptionFunction) => 195 | >( 196 | operation: O, 197 | graphqlOptions?: ThunderGraphQLOptions, 198 | ) => 199 | ( 200 | o: (Z & ValueTypes[R]) | ValueTypes[R], 201 | ops?: OperationOptions & { variables?: ExtractVariables }, 202 | ) => { 203 | const returnedFunction = fn( 204 | Zeus(operation, o, { 205 | operationOptions: ops, 206 | scalars: graphqlOptions?.scalars, 207 | }), 208 | ) as SubscriptionToGraphQL; 209 | if (returnedFunction?.on && graphqlOptions?.scalars) { 210 | const wrapped = returnedFunction.on; 211 | returnedFunction.on = (fnToCall: (args: InputType) => void) => 212 | wrapped((data: InputType) => { 213 | if (graphqlOptions?.scalars) { 214 | return fnToCall( 215 | decodeScalarsInResponse({ 216 | response: data, 217 | initialOp: operation, 218 | initialZeusQuery: o as VType, 219 | returns: ReturnTypes, 220 | scalars: graphqlOptions.scalars, 221 | ops: Ops, 222 | }), 223 | ); 224 | } 225 | return fnToCall(data); 226 | }); 227 | } 228 | return returnedFunction; 229 | }; 230 | 231 | export const Subscription = (...options: chainOptions) => SubscriptionThunder(apiSubscription(options)); 232 | export const Zeus = < 233 | Z extends ValueTypes[R], 234 | O extends keyof typeof Ops, 235 | R extends keyof ValueTypes = GenericOperation, 236 | >( 237 | operation: O, 238 | o: (Z & ValueTypes[R]) | ValueTypes[R], 239 | ops?: { 240 | operationOptions?: OperationOptions; 241 | scalars?: ScalarDefinition; 242 | }, 243 | ) => 244 | InternalsBuildQuery({ 245 | props: AllTypesProps, 246 | returns: ReturnTypes, 247 | ops: Ops, 248 | options: ops?.operationOptions, 249 | scalars: ops?.scalars, 250 | })(operation, o as VType); 251 | 252 | export const ZeusSelect = () => ((t: unknown) => t) as SelectionFunction; 253 | 254 | export const Selector = (key: T) => key && ZeusSelect(); 255 | 256 | export const TypeFromSelector = (key: T) => key && ZeusSelect(); 257 | export const Gql = Chain(HOST, { 258 | headers: { 259 | 'Content-Type': 'application/json', 260 | ...HEADERS, 261 | }, 262 | }); 263 | 264 | export const ZeusScalars = ZeusSelect(); 265 | 266 | export const decodeScalarsInResponse = ({ 267 | response, 268 | scalars, 269 | returns, 270 | ops, 271 | initialZeusQuery, 272 | initialOp, 273 | }: { 274 | ops: O; 275 | response: any; 276 | returns: ReturnTypesType; 277 | scalars?: Record; 278 | initialOp: keyof O; 279 | initialZeusQuery: InputValueType | VType; 280 | }) => { 281 | if (!scalars) { 282 | return response; 283 | } 284 | const builder = PrepareScalarPaths({ 285 | ops, 286 | returns, 287 | }); 288 | 289 | const scalarPaths = builder(initialOp as string, ops[initialOp], initialZeusQuery); 290 | if (scalarPaths) { 291 | const r = traverseResponse({ scalarPaths, resolvers: scalars })(initialOp as string, response, [ops[initialOp]]); 292 | return r; 293 | } 294 | return response; 295 | }; 296 | 297 | export const traverseResponse = ({ 298 | resolvers, 299 | scalarPaths, 300 | }: { 301 | scalarPaths: { [x: string]: `scalar.${string}` }; 302 | resolvers: { 303 | [x: string]: ScalarResolver | undefined; 304 | }; 305 | }) => { 306 | const ibb = (k: string, o: InputValueType | VType, p: string[] = []): unknown => { 307 | if (Array.isArray(o)) { 308 | return o.map((eachO) => ibb(k, eachO, p)); 309 | } 310 | if (o == null) { 311 | return o; 312 | } 313 | const scalarPathString = p.join(SEPARATOR); 314 | const currentScalarString = scalarPaths[scalarPathString]; 315 | if (currentScalarString) { 316 | const currentDecoder = resolvers[currentScalarString.split('.')[1]]?.decode; 317 | if (currentDecoder) { 318 | return currentDecoder(o); 319 | } 320 | } 321 | if (typeof o === 'boolean' || typeof o === 'number' || typeof o === 'string' || !o) { 322 | return o; 323 | } 324 | const entries = Object.entries(o).map(([k, v]) => [k, ibb(k, v, [...p, purifyGraphQLKey(k)])] as const); 325 | const objectFromEntries = entries.reduce>((a, [k, v]) => { 326 | a[k] = v; 327 | return a; 328 | }, {}); 329 | return objectFromEntries; 330 | }; 331 | return ibb; 332 | }; 333 | 334 | export type AllTypesPropsType = { 335 | [x: string]: 336 | | undefined 337 | | `scalar.${string}` 338 | | 'enum' 339 | | { 340 | [x: string]: 341 | | undefined 342 | | string 343 | | { 344 | [x: string]: string | undefined; 345 | }; 346 | }; 347 | }; 348 | 349 | export type ReturnTypesType = { 350 | [x: string]: 351 | | { 352 | [x: string]: string | undefined; 353 | } 354 | | `scalar.${string}` 355 | | undefined; 356 | }; 357 | export type InputValueType = { 358 | [x: string]: undefined | boolean | string | number | [any, undefined | boolean | InputValueType] | InputValueType; 359 | }; 360 | export type VType = 361 | | undefined 362 | | boolean 363 | | string 364 | | number 365 | | [any, undefined | boolean | InputValueType] 366 | | InputValueType; 367 | 368 | export type PlainType = boolean | number | string | null | undefined; 369 | export type ZeusArgsType = 370 | | PlainType 371 | | { 372 | [x: string]: ZeusArgsType; 373 | } 374 | | Array; 375 | 376 | export type Operations = Record; 377 | 378 | export type VariableDefinition = { 379 | [x: string]: unknown; 380 | }; 381 | 382 | export const SEPARATOR = '|'; 383 | 384 | export type fetchOptions = Parameters; 385 | type websocketOptions = typeof WebSocket extends new (...args: infer R) => WebSocket ? R : never; 386 | export type chainOptions = [fetchOptions[0], fetchOptions[1] & { websocket?: websocketOptions }] | [fetchOptions[0]]; 387 | export type FetchFunction = (query: string, variables?: Record) => Promise; 388 | export type SubscriptionFunction = (query: string) => any; 389 | type NotUndefined = T extends undefined ? never : T; 390 | export type ResolverType = NotUndefined; 391 | 392 | export type OperationOptions = { 393 | operationName?: string; 394 | }; 395 | 396 | export type ScalarCoder = Record string>; 397 | 398 | export interface GraphQLResponse { 399 | data?: Record; 400 | errors?: Array<{ 401 | message: string; 402 | }>; 403 | } 404 | export class GraphQLError extends Error { 405 | constructor(public response: GraphQLResponse) { 406 | super(''); 407 | console.error(response); 408 | } 409 | toString() { 410 | return 'GraphQL Response Error'; 411 | } 412 | } 413 | export type GenericOperation = O extends keyof typeof Ops ? typeof Ops[O] : never; 414 | export type ThunderGraphQLOptions = { 415 | scalars?: SCLR | ScalarCoders; 416 | }; 417 | 418 | const ExtractScalar = (mappedParts: string[], returns: ReturnTypesType): `scalar.${string}` | undefined => { 419 | if (mappedParts.length === 0) { 420 | return; 421 | } 422 | const oKey = mappedParts[0]; 423 | const returnP1 = returns[oKey]; 424 | if (typeof returnP1 === 'object') { 425 | const returnP2 = returnP1[mappedParts[1]]; 426 | if (returnP2) { 427 | return ExtractScalar([returnP2, ...mappedParts.slice(2)], returns); 428 | } 429 | return undefined; 430 | } 431 | return returnP1 as `scalar.${string}` | undefined; 432 | }; 433 | 434 | export const PrepareScalarPaths = ({ ops, returns }: { returns: ReturnTypesType; ops: Operations }) => { 435 | const ibb = ( 436 | k: string, 437 | originalKey: string, 438 | o: InputValueType | VType, 439 | p: string[] = [], 440 | pOriginals: string[] = [], 441 | root = true, 442 | ): { [x: string]: `scalar.${string}` } | undefined => { 443 | if (!o) { 444 | return; 445 | } 446 | if (typeof o === 'boolean' || typeof o === 'number' || typeof o === 'string') { 447 | const extractionArray = [...pOriginals, originalKey]; 448 | const isScalar = ExtractScalar(extractionArray, returns); 449 | if (isScalar?.startsWith('scalar')) { 450 | const partOfTree = { 451 | [[...p, k].join(SEPARATOR)]: isScalar, 452 | }; 453 | return partOfTree; 454 | } 455 | return {}; 456 | } 457 | if (Array.isArray(o)) { 458 | return ibb(k, k, o[1], p, pOriginals, false); 459 | } 460 | if (k === '__alias') { 461 | return Object.entries(o) 462 | .map(([alias, objectUnderAlias]) => { 463 | if (typeof objectUnderAlias !== 'object' || Array.isArray(objectUnderAlias)) { 464 | throw new Error( 465 | 'Invalid alias it should be __alias:{ YOUR_ALIAS_NAME: { OPERATION_NAME: { ...selectors }}}', 466 | ); 467 | } 468 | const operationName = Object.keys(objectUnderAlias)[0]; 469 | const operation = objectUnderAlias[operationName]; 470 | return ibb(alias, operationName, operation, p, pOriginals, false); 471 | }) 472 | .reduce((a, b) => ({ 473 | ...a, 474 | ...b, 475 | })); 476 | } 477 | const keyName = root ? ops[k] : k; 478 | return Object.entries(o) 479 | .filter(([k]) => k !== '__directives') 480 | .map(([k, v]) => { 481 | // Inline fragments shouldn't be added to the path as they aren't a field 482 | const isInlineFragment = originalKey.match(/^...\s*on/) != null; 483 | return ibb( 484 | k, 485 | k, 486 | v, 487 | isInlineFragment ? p : [...p, purifyGraphQLKey(keyName || k)], 488 | isInlineFragment ? pOriginals : [...pOriginals, purifyGraphQLKey(originalKey)], 489 | false, 490 | ); 491 | }) 492 | .reduce((a, b) => ({ 493 | ...a, 494 | ...b, 495 | })); 496 | }; 497 | return ibb; 498 | }; 499 | 500 | export const purifyGraphQLKey = (k: string) => k.replace(/\([^)]*\)/g, '').replace(/^[^:]*\:/g, ''); 501 | 502 | const mapPart = (p: string) => { 503 | const [isArg, isField] = p.split('<>'); 504 | if (isField) { 505 | return { 506 | v: isField, 507 | __type: 'field', 508 | } as const; 509 | } 510 | return { 511 | v: isArg, 512 | __type: 'arg', 513 | } as const; 514 | }; 515 | 516 | type Part = ReturnType; 517 | 518 | export const ResolveFromPath = (props: AllTypesPropsType, returns: ReturnTypesType, ops: Operations) => { 519 | const ResolvePropsType = (mappedParts: Part[]) => { 520 | const oKey = ops[mappedParts[0].v]; 521 | const propsP1 = oKey ? props[oKey] : props[mappedParts[0].v]; 522 | if (propsP1 === 'enum' && mappedParts.length === 1) { 523 | return 'enum'; 524 | } 525 | if (typeof propsP1 === 'string' && propsP1.startsWith('scalar.') && mappedParts.length === 1) { 526 | return propsP1; 527 | } 528 | if (typeof propsP1 === 'object') { 529 | if (mappedParts.length < 2) { 530 | return 'not'; 531 | } 532 | const propsP2 = propsP1[mappedParts[1].v]; 533 | if (typeof propsP2 === 'string') { 534 | return rpp( 535 | `${propsP2}${SEPARATOR}${mappedParts 536 | .slice(2) 537 | .map((mp) => mp.v) 538 | .join(SEPARATOR)}`, 539 | ); 540 | } 541 | if (typeof propsP2 === 'object') { 542 | if (mappedParts.length < 3) { 543 | return 'not'; 544 | } 545 | const propsP3 = propsP2[mappedParts[2].v]; 546 | if (propsP3 && mappedParts[2].__type === 'arg') { 547 | return rpp( 548 | `${propsP3}${SEPARATOR}${mappedParts 549 | .slice(3) 550 | .map((mp) => mp.v) 551 | .join(SEPARATOR)}`, 552 | ); 553 | } 554 | } 555 | } 556 | }; 557 | const ResolveReturnType = (mappedParts: Part[]) => { 558 | if (mappedParts.length === 0) { 559 | return 'not'; 560 | } 561 | const oKey = ops[mappedParts[0].v]; 562 | const returnP1 = oKey ? returns[oKey] : returns[mappedParts[0].v]; 563 | if (typeof returnP1 === 'object') { 564 | if (mappedParts.length < 2) return 'not'; 565 | const returnP2 = returnP1[mappedParts[1].v]; 566 | if (returnP2) { 567 | return rpp( 568 | `${returnP2}${SEPARATOR}${mappedParts 569 | .slice(2) 570 | .map((mp) => mp.v) 571 | .join(SEPARATOR)}`, 572 | ); 573 | } 574 | } 575 | }; 576 | const rpp = (path: string): 'enum' | 'not' | `scalar.${string}` => { 577 | const parts = path.split(SEPARATOR).filter((l) => l.length > 0); 578 | const mappedParts = parts.map(mapPart); 579 | const propsP1 = ResolvePropsType(mappedParts); 580 | if (propsP1) { 581 | return propsP1; 582 | } 583 | const returnP1 = ResolveReturnType(mappedParts); 584 | if (returnP1) { 585 | return returnP1; 586 | } 587 | return 'not'; 588 | }; 589 | return rpp; 590 | }; 591 | 592 | export const InternalArgsBuilt = ({ 593 | props, 594 | ops, 595 | returns, 596 | scalars, 597 | vars, 598 | }: { 599 | props: AllTypesPropsType; 600 | returns: ReturnTypesType; 601 | ops: Operations; 602 | scalars?: ScalarDefinition; 603 | vars: Array<{ name: string; graphQLType: string }>; 604 | }) => { 605 | const arb = (a: ZeusArgsType, p = '', root = true): string => { 606 | if (typeof a === 'string') { 607 | if (a.startsWith(START_VAR_NAME)) { 608 | const [varName, graphQLType] = a.replace(START_VAR_NAME, '$').split(GRAPHQL_TYPE_SEPARATOR); 609 | const v = vars.find((v) => v.name === varName); 610 | if (!v) { 611 | vars.push({ 612 | name: varName, 613 | graphQLType, 614 | }); 615 | } else { 616 | if (v.graphQLType !== graphQLType) { 617 | throw new Error( 618 | `Invalid variable exists with two different GraphQL Types, "${v.graphQLType}" and ${graphQLType}`, 619 | ); 620 | } 621 | } 622 | return varName; 623 | } 624 | } 625 | const checkType = ResolveFromPath(props, returns, ops)(p); 626 | if (checkType.startsWith('scalar.')) { 627 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 628 | const [_, ...splittedScalar] = checkType.split('.'); 629 | const scalarKey = splittedScalar.join('.'); 630 | return (scalars?.[scalarKey]?.encode?.(a) as string) || JSON.stringify(a); 631 | } 632 | if (Array.isArray(a)) { 633 | return `[${a.map((arr) => arb(arr, p, false)).join(', ')}]`; 634 | } 635 | if (typeof a === 'string') { 636 | if (checkType === 'enum') { 637 | return a; 638 | } 639 | return `${JSON.stringify(a)}`; 640 | } 641 | if (typeof a === 'object') { 642 | if (a === null) { 643 | return `null`; 644 | } 645 | const returnedObjectString = Object.entries(a) 646 | .filter(([, v]) => typeof v !== 'undefined') 647 | .map(([k, v]) => `${k}: ${arb(v, [p, k].join(SEPARATOR), false)}`) 648 | .join(',\n'); 649 | if (!root) { 650 | return `{${returnedObjectString}}`; 651 | } 652 | return returnedObjectString; 653 | } 654 | return `${a}`; 655 | }; 656 | return arb; 657 | }; 658 | 659 | export const resolverFor = ( 660 | type: T, 661 | field: Z, 662 | fn: ( 663 | args: Required[Z] extends [infer Input, any] ? Input : any, 664 | source: any, 665 | ) => Z extends keyof ModelTypes[T] ? ModelTypes[T][Z] | Promise | X : never, 666 | ) => fn as (args?: any, source?: any) => ReturnType; 667 | 668 | export type UnwrapPromise = T extends Promise ? R : T; 669 | export type ZeusState Promise> = NonNullable>>; 670 | export type ZeusHook< 671 | T extends (...args: any[]) => Record Promise>, 672 | N extends keyof ReturnType, 673 | > = ZeusState[N]>; 674 | 675 | export type WithTypeNameValue = T & { 676 | __typename?: boolean; 677 | __directives?: string; 678 | }; 679 | export type AliasType = WithTypeNameValue & { 680 | __alias?: Record>; 681 | }; 682 | type DeepAnify = { 683 | [P in keyof T]?: any; 684 | }; 685 | type IsPayLoad = T extends [any, infer PayLoad] ? PayLoad : T; 686 | export type ScalarDefinition = Record; 687 | 688 | type IsScalar = S extends 'scalar' & { name: infer T } 689 | ? T extends keyof SCLR 690 | ? SCLR[T]['decode'] extends (s: unknown) => unknown 691 | ? ReturnType 692 | : unknown 693 | : unknown 694 | : S; 695 | type IsArray = T extends Array 696 | ? InputType[] 697 | : InputType; 698 | type FlattenArray = T extends Array ? R : T; 699 | type BaseZeusResolver = boolean | 1 | string | Variable; 700 | 701 | type IsInterfaced, DST, SCLR extends ScalarDefinition> = FlattenArray extends 702 | | ZEUS_INTERFACES 703 | | ZEUS_UNIONS 704 | ? { 705 | [P in keyof SRC]: SRC[P] extends '__union' & infer R 706 | ? P extends keyof DST 707 | ? IsArray 708 | : IsArray, SCLR> 709 | : never; 710 | }[keyof SRC] & { 711 | [P in keyof Omit< 712 | Pick< 713 | SRC, 714 | { 715 | [P in keyof DST]: SRC[P] extends '__union' & infer R ? never : P; 716 | }[keyof DST] 717 | >, 718 | '__typename' 719 | >]: IsPayLoad extends BaseZeusResolver ? IsScalar : IsArray; 720 | } 721 | : { 722 | [P in keyof Pick]: IsPayLoad extends BaseZeusResolver 723 | ? IsScalar 724 | : IsArray; 725 | }; 726 | 727 | export type MapType = SRC extends DeepAnify 728 | ? IsInterfaced 729 | : never; 730 | // eslint-disable-next-line @typescript-eslint/ban-types 731 | export type InputType = IsPayLoad extends { __alias: infer R } 732 | ? { 733 | [P in keyof R]: MapType[keyof MapType]; 734 | } & MapType, '__alias'>, SCLR> 735 | : MapType, SCLR>; 736 | export type SubscriptionToGraphQL = { 737 | ws: WebSocket; 738 | on: (fn: (args: InputType) => void) => void; 739 | off: (fn: (e: { data?: InputType; code?: number; reason?: string; message?: string }) => void) => void; 740 | error: (fn: (e: { data?: InputType; errors?: string[] }) => void) => void; 741 | open: () => void; 742 | }; 743 | 744 | // eslint-disable-next-line @typescript-eslint/ban-types 745 | export type FromSelector = InputType< 746 | GraphQLTypes[NAME], 747 | SELECTOR, 748 | SCLR 749 | >; 750 | 751 | export type ScalarResolver = { 752 | encode?: (s: unknown) => string; 753 | decode?: (s: unknown) => unknown; 754 | }; 755 | 756 | export type SelectionFunction = (t: T | V) => T; 757 | 758 | type BuiltInVariableTypes = { 759 | ['String']: string; 760 | ['Int']: number; 761 | ['Float']: number; 762 | ['ID']: unknown; 763 | ['Boolean']: boolean; 764 | }; 765 | type AllVariableTypes = keyof BuiltInVariableTypes | keyof ZEUS_VARIABLES; 766 | type VariableRequired = `${T}!` | T | `[${T}]` | `[${T}]!` | `[${T}!]` | `[${T}!]!`; 767 | type VR = VariableRequired>; 768 | 769 | export type GraphQLVariableType = VR; 770 | 771 | type ExtractVariableTypeString = T extends VR 772 | ? R1 extends VR 773 | ? R2 extends VR 774 | ? R3 extends VR 775 | ? R4 extends VR 776 | ? R5 777 | : R4 778 | : R3 779 | : R2 780 | : R1 781 | : T; 782 | 783 | type DecomposeType = T extends `[${infer R}]` 784 | ? Array> | undefined 785 | : T extends `${infer R}!` 786 | ? NonNullable> 787 | : Type | undefined; 788 | 789 | type ExtractTypeFromGraphQLType = T extends keyof ZEUS_VARIABLES 790 | ? ZEUS_VARIABLES[T] 791 | : T extends keyof BuiltInVariableTypes 792 | ? BuiltInVariableTypes[T] 793 | : any; 794 | 795 | export type GetVariableType = DecomposeType< 796 | T, 797 | ExtractTypeFromGraphQLType> 798 | >; 799 | 800 | type UndefinedKeys = { 801 | [K in keyof T]-?: T[K] extends NonNullable ? never : K; 802 | }[keyof T]; 803 | 804 | type WithNullableKeys = Pick>; 805 | type WithNonNullableKeys = Omit>; 806 | 807 | type OptionalKeys = { 808 | [P in keyof T]?: T[P]; 809 | }; 810 | 811 | export type WithOptionalNullables = OptionalKeys> & WithNonNullableKeys; 812 | 813 | export type Variable = { 814 | ' __zeus_name': Name; 815 | ' __zeus_type': T; 816 | }; 817 | 818 | export type ExtractVariables = Query extends Variable 819 | ? { [key in VName]: GetVariableType } 820 | : Query extends [infer Inputs, infer Outputs] 821 | ? ExtractVariables & ExtractVariables 822 | : Query extends string | number | boolean 823 | ? // eslint-disable-next-line @typescript-eslint/ban-types 824 | {} 825 | : UnionToIntersection<{ [K in keyof Query]: WithOptionalNullables> }[keyof Query]>; 826 | 827 | type UnionToIntersection = (U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never; 828 | 829 | export const START_VAR_NAME = `$ZEUS_VAR`; 830 | export const GRAPHQL_TYPE_SEPARATOR = `__$GRAPHQL__`; 831 | 832 | export const $ = (name: Name, graphqlType: Type) => { 833 | return (START_VAR_NAME + name + GRAPHQL_TYPE_SEPARATOR + graphqlType) as unknown as Variable; 834 | }; 835 | type ZEUS_INTERFACES = never 836 | export type ScalarCoders = { 837 | DateTime?: ScalarResolver; 838 | } 839 | type ZEUS_UNIONS = never 840 | 841 | export type ValueTypes = { 842 | ["User"]: AliasType<{ 843 | id?:boolean | `@${string}`, 844 | provider?:boolean | `@${string}`, 845 | providerId?:boolean | `@${string}`, 846 | username?:boolean | `@${string}`, 847 | name?:boolean | `@${string}`, 848 | orders?:ValueTypes["Order"], 849 | created_at?:boolean | `@${string}`, 850 | updated_at?:boolean | `@${string}`, 851 | __typename?: boolean | `@${string}` 852 | }>; 853 | /** A date-time string at UTC, such as 2019-12-03T09:54:33Z, compliant with the date-time format. */ 854 | ["DateTime"]:unknown; 855 | ["Order"]: AliasType<{ 856 | id?:boolean | `@${string}`, 857 | alias?:boolean | `@${string}`, 858 | user?:ValueTypes["User"], 859 | thing?:ValueTypes["Thing"], 860 | created_at?:boolean | `@${string}`, 861 | updated_at?:boolean | `@${string}`, 862 | __typename?: boolean | `@${string}` 863 | }>; 864 | ["Thing"]: AliasType<{ 865 | id?:boolean | `@${string}`, 866 | name?:boolean | `@${string}`, 867 | orders?:ValueTypes["Order"], 868 | created_at?:boolean | `@${string}`, 869 | updated_at?:boolean | `@${string}`, 870 | __typename?: boolean | `@${string}` 871 | }>; 872 | ["Query"]: AliasType<{ 873 | users?:ValueTypes["User"], 874 | whoAmI?:ValueTypes["User"], 875 | things?:ValueTypes["Thing"], 876 | orders?:ValueTypes["Order"], 877 | __typename?: boolean | `@${string}` 878 | }>; 879 | ["Mutation"]: AliasType<{ 880 | createOrder?: [{ thingName: string | Variable, alias: string | Variable},ValueTypes["Order"]], 881 | __typename?: boolean | `@${string}` 882 | }> 883 | } 884 | 885 | export type ResolverInputTypes = { 886 | ["User"]: AliasType<{ 887 | id?:boolean | `@${string}`, 888 | provider?:boolean | `@${string}`, 889 | providerId?:boolean | `@${string}`, 890 | username?:boolean | `@${string}`, 891 | name?:boolean | `@${string}`, 892 | orders?:ResolverInputTypes["Order"], 893 | created_at?:boolean | `@${string}`, 894 | updated_at?:boolean | `@${string}`, 895 | __typename?: boolean | `@${string}` 896 | }>; 897 | /** A date-time string at UTC, such as 2019-12-03T09:54:33Z, compliant with the date-time format. */ 898 | ["DateTime"]:unknown; 899 | ["Order"]: AliasType<{ 900 | id?:boolean | `@${string}`, 901 | alias?:boolean | `@${string}`, 902 | user?:ResolverInputTypes["User"], 903 | thing?:ResolverInputTypes["Thing"], 904 | created_at?:boolean | `@${string}`, 905 | updated_at?:boolean | `@${string}`, 906 | __typename?: boolean | `@${string}` 907 | }>; 908 | ["Thing"]: AliasType<{ 909 | id?:boolean | `@${string}`, 910 | name?:boolean | `@${string}`, 911 | orders?:ResolverInputTypes["Order"], 912 | created_at?:boolean | `@${string}`, 913 | updated_at?:boolean | `@${string}`, 914 | __typename?: boolean | `@${string}` 915 | }>; 916 | ["Query"]: AliasType<{ 917 | users?:ResolverInputTypes["User"], 918 | whoAmI?:ResolverInputTypes["User"], 919 | things?:ResolverInputTypes["Thing"], 920 | orders?:ResolverInputTypes["Order"], 921 | __typename?: boolean | `@${string}` 922 | }>; 923 | ["Mutation"]: AliasType<{ 924 | createOrder?: [{ thingName: string, alias: string},ResolverInputTypes["Order"]], 925 | __typename?: boolean | `@${string}` 926 | }>; 927 | ["schema"]: AliasType<{ 928 | query?:ResolverInputTypes["Query"], 929 | mutation?:ResolverInputTypes["Mutation"], 930 | __typename?: boolean | `@${string}` 931 | }> 932 | } 933 | 934 | export type ModelTypes = { 935 | ["User"]: { 936 | id: number, 937 | provider: string, 938 | providerId: string, 939 | username: string, 940 | name: string, 941 | orders: Array, 942 | created_at: ModelTypes["DateTime"], 943 | updated_at: ModelTypes["DateTime"] 944 | }; 945 | /** A date-time string at UTC, such as 2019-12-03T09:54:33Z, compliant with the date-time format. */ 946 | ["DateTime"]:any; 947 | ["Order"]: { 948 | id: number, 949 | alias: string, 950 | user: ModelTypes["User"], 951 | thing: ModelTypes["Thing"], 952 | created_at: ModelTypes["DateTime"], 953 | updated_at: ModelTypes["DateTime"] 954 | }; 955 | ["Thing"]: { 956 | id: number, 957 | name: string, 958 | orders: Array, 959 | created_at: ModelTypes["DateTime"], 960 | updated_at: ModelTypes["DateTime"] 961 | }; 962 | ["Query"]: { 963 | users: Array, 964 | whoAmI: ModelTypes["User"], 965 | things: Array, 966 | orders: Array 967 | }; 968 | ["Mutation"]: { 969 | createOrder: ModelTypes["Order"] 970 | }; 971 | ["schema"]: { 972 | query?: ModelTypes["Query"] | undefined, 973 | mutation?: ModelTypes["Mutation"] | undefined 974 | } 975 | } 976 | 977 | export type GraphQLTypes = { 978 | // ------------------------------------------------------; 979 | // THIS FILE WAS AUTOMATICALLY GENERATED (DO NOT MODIFY); 980 | // ------------------------------------------------------; 981 | ["User"]: { 982 | __typename: "User", 983 | id: number, 984 | provider: string, 985 | providerId: string, 986 | username: string, 987 | name: string, 988 | orders: Array, 989 | created_at: GraphQLTypes["DateTime"], 990 | updated_at: GraphQLTypes["DateTime"] 991 | }; 992 | /** A date-time string at UTC, such as 2019-12-03T09:54:33Z, compliant with the date-time format. */ 993 | ["DateTime"]: "scalar" & { name: "DateTime" }; 994 | ["Order"]: { 995 | __typename: "Order", 996 | id: number, 997 | alias: string, 998 | user: GraphQLTypes["User"], 999 | thing: GraphQLTypes["Thing"], 1000 | created_at: GraphQLTypes["DateTime"], 1001 | updated_at: GraphQLTypes["DateTime"] 1002 | }; 1003 | ["Thing"]: { 1004 | __typename: "Thing", 1005 | id: number, 1006 | name: string, 1007 | orders: Array, 1008 | created_at: GraphQLTypes["DateTime"], 1009 | updated_at: GraphQLTypes["DateTime"] 1010 | }; 1011 | ["Query"]: { 1012 | __typename: "Query", 1013 | users: Array, 1014 | whoAmI: GraphQLTypes["User"], 1015 | things: Array, 1016 | orders: Array 1017 | }; 1018 | ["Mutation"]: { 1019 | __typename: "Mutation", 1020 | createOrder: GraphQLTypes["Order"] 1021 | } 1022 | } 1023 | 1024 | 1025 | type ZEUS_VARIABLES = { 1026 | ["DateTime"]: ValueTypes["DateTime"]; 1027 | } --------------------------------------------------------------------------------