Skip to content

Instantly share code, notes, and snippets.

@VicAv99
Created April 23, 2019 22:29
Show Gist options
  • Save VicAv99/a3eb2b17ed6edc5f1988ccfef4312ab4 to your computer and use it in GitHub Desktop.
Save VicAv99/a3eb2b17ed6edc5f1988ccfef4312ab4 to your computer and use it in GitHub Desktop.

This documentation is meant to notate the build process starting from simply generating a project directory all the way to finishing with a full master detail application. This application is using the “Sparkle Stack”, which is best used in cases where a full application environment is needed (database to server to frontend). Before each step of the project below, a quick overview of technologies used will be described along with and links and references.

Root Directory

The root level of the application is meant as a container for our Frontend (client) and Backend (server) applications, however we also take advantage of this directory to define any workspace rules based on languages. More importantly, we compose Docker at this level.

Technologies:

  • Makefile
  • Docker
  • editorconfig/prettier

Setup:

  • mkdir sparkle-stack root directory
    • touch .editorconfig .gitignore .nvmrc .prettierrc .prettierignore README.md
    • touch docker-compose.yml Makefile tslint.json
# Makefile
help: ## Help documentation
	@echo "Available targets:"
	@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'

audit: ## Run "yarn audit" in all projects
	@(cd client && yarn audit)
	@(cd server && yarn audit)

docker-clean: ## Clean up the last containers for this project
	@docker-compose down --rmi local -v --remove-orphans

install: ## Run "yarn" in all projects
	@(cd client && yarn)
	@(cd server && yarn)

lint: ## Run "yarn lint" in all projects
	@(cd client && yarn run lint)
	@(cd server && yarn run lint)

start: ## Start the containers
	@(COMPOSE_HTTP_TIMEOUT=$$COMPOSE_HTTP_TIMEOUT docker-compose up --remove-orphans --build)

start-clean: docker-clean start ## Clean the docker containers then start
#docker-compose.yml
version: '3'

services:
  client:
    build:
      context: ./client
    depends_on:
      - server
    environment:
      - NODE_ENV=development
      - BASE_URL=http://server:8080
    links:
      - server:server
    volumes:
      - ./client:/usr/app
      - /usr/app/node_modules
    ports:
      - 4200:4200
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:4200"]
      interval: 30s
      timeout: 10s
      retries: 3

  server:
    build: ./server
    volumes:
      - ./server:/usr/app/
      - /usr/app/node_modules
    ports:
      - 8080:3000
    depends_on:
      - postgres
    links:
      - postgres:postgres
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3000"]
      interval: 30s
      timeout: 10s
      retries: 3
    env_file:
      - ./server/.env
    environment:
      NODE_ENV: development
      DATABASE_URL: postgres://root:postgres@postgres/public

  postgres:
    image: postgres:alpine
    restart: always
    environment:
      POSTGRES_DB: public
      POSTGRES_USER: root
    healthcheck:
      test: ['CMD-SHELL', "pg_isready --dbname public --username=root"]
      interval: 30s
      timeout: 10s
      retries: 3
    ports:
      - 5433:5432
    volumes:
      - postgres:/var/lib/postgresql/data

volumes:
  postgres:

Client

For the frontend project we are taking advantage of the Angular CLI and orchestrating the application(s) with Nrwl extensions (great for mono-repos). The Sparkle Stack uses NGRX for state management and GraphQL via Apollo for the data layer. We are also using Angular Material for all the core UI components.

Setup:

  • create-nx-workspace client
  • cd client
  • ng new items
  • Generate application:
    • ng generate app items generates application shell
    • ng generate component items -m app
    • ng generate component items-list -m app
    • ng generate component items-details -m app
    • ng generate module routing -m app
  • Generate common libraries:
    • ng generate lib core-data data layer via GraphQL
    • ng generate lib core-state state management with NGRX
    • ng generate lib graphql initialize GraphQL
    • ng generate lib material setup Angular Material
    • ng generate lib ui-login common login component
    • ng generate lib ui-toolbar common toolbar component
  • Continue Docker implementations:
    • touch .dockerignore Dockerfile
#.dockerignore
.DS_Store
.github
.vscode
coverage
JenkinsFile
node_modules
npm-debug.log
#Dockerfile
FROM node:10-alpine

WORKDIR /usr/app

/# ng serve port/
EXPOSE 4200

/# Hot reloading/
EXPOSE 49153

RUN apk add --no-cache \
  bash \
  libsass \
  git

COPY package.json .

RUN echo "export PATH=$PATH:/usr/app/node_modules/.bin:" > /etc/environment && yarn

COPY . .

CMD ["npm", "start", "items"]

Server

The server takes advantage of NestJS on top of Express. NestJS is optimal here because as a server framework, it was built to mimic Angular (NestJS is a very Angular-esk NodeJS framework). We also take advantage of GraphQL via the Apollo-Server which handles our queries and mutations based on specified resolvers. The data layer is handled by PostgresDB and Sequelize of the ORM. We can also set up basic role authentication using JWT/Passport for the authentication and role handling.

Install:

  • npm install @nestjs/graphql @nestjs/jwt @nestjs/passport @nestjs/platform-express config faker passport passport-jwt pg sequelize sequelize-typescript sequelize-cli umzug
  • npm install -D @types/sequelize

Setup:

  • nest new server
  • cd server
    • Config:
      • mkdir config
      • touch config/default.json && config/development.json
// default/development.json
{
  "database": {
    "url": "postgres://root:postgres@postgres/public"
  },
  "jwt": {
    "secret": "SECRET"
  }
}
  • touch .env
    • Sequelize:
      • Migrations:
        • mkdir migrations
        • sequelize migration:generate —name create-uuid-extension
'use strict';

module.exports = {
  up: async (queryInterface, Sequelize) => {
    await queryInterface.sequelize.query('CREATE EXTENSION IF NOT EXISTS "uuid-ossp";')
  },
  down: async (queryInterface, Sequelize) => {
    await queryInterface.sequelize.query('DROP EXTENSION IF EXISTS "uuid-ossp";')
  }
};
  • sequelize migration:generate —name create-users
'use strict';

const tableName = 'users';

module.exports = {
  up: async (queryInterface, Sequelize) => {
    await queryInterface.createTable(tableName, {
      id: {
        allowNull: false,
        defaultValue: Sequelize.fn('uuid_generate_v4'),
        primaryKey: true,
        type: Sequelize.UUID
      },
      username: {
        unique: true,
        allowNull: false,
        type: Sequelize.STRING
      },
      password: {
        allowNull: false,
        type: Sequelize.STRING
      },
      role: {
        allowNull: true,
        defaultValue: 'User',
        type: Sequelize.ENUM('User', 'Supervisor', 'Admin')
      },
      createdAt: {
        allowNull: false,
        defaultValue: Sequelize.fn('now'),
        type: Sequelize.DATE
      },
      updatedAt: {
        type: Sequelize.DATE,
        allowNull: true,
        defaultValue: Sequelize.fn('now')
      }
    });
  },

  down: async (queryInterface, Sequelize) => {
    await queryInterface.dropTable(tableName);
  }
};
  • sequelize migration:generate —name create-items
'use strict';

const tableName = 'items';

module.exports = {
  up: async (queryInterface, Sequelize) => {
    await queryInterface.createTable(tableName, {
      id: {
        primaryKey: true,
        allowNull: false,
        type: Sequelize.UUID,
        defaultValue: Sequelize.fn('uuid_generate_v4'),
      },
      name: {
        type: Sequelize.STRING,
      },
      description: {
        allowNull: true,
        type: Sequelize.STRING,
      }
    });
  },

  down: async (queryInterface, Sequelize) => {
    return queryInterface.dropTable(tableName);
  }
};
  • nest g s core/migrations
import { Injectable, Logger } from '@nestjs/common';

import * as Umzug from 'umzug';
import { Sequelize } from 'sequelize-typescript';

import seedData from '../../../seeds/seeds';

@Injectable()
export class MigrationsService {
  async migrateDatabase(sequelize: Sequelize) {
    Logger.log('Checking database for migrations');

    const ummie = new Umzug({
      logging: (message: any) => Logger.log(message),
      migrations: {
        params: [sequelize.getQueryInterface(), Sequelize],
      },
      storage: 'sequelize',
      storageOptions: {
        sequelize,
        tableName: 'migrations',
      },
    });

    const migrations = await ummie.pending();

    Logger.log(`${migrations.length} migration(s) to run.`);

    if (migrations.length) {
      Logger.log('Running migrations', 'Migration Service');
      await ummie.up();
      Logger.log('Migrations complete', 'Migration Service');
    }

    Logger.log('Database seed starting');

    await seedData();

    Logger.log('Database seed complete');
  }
}
  • Entities:
    • touch src/core/entities/item.ts && touch src/core/entities/user.ts
// user.ts
import { Table, Model, PrimaryKey, AllowNull, Default, Sequelize, Column, DataType, Unique, CreatedAt, UpdatedAt } from 'sequelize-typescript';
import { UserRole } from './entity-utils/user-role.enum';

@Table({modelName: 'users'})
export class User extends Model<User> {
  @PrimaryKey
  @AllowNull(false)
  @Default(Sequelize.fn('uuid_generate_v4'))
  @Column({type: DataType.UUID})
  id: string;

  @Unique
  @AllowNull(false)
  @Column({type: DataType.STRING})
  username: string;

  @AllowNull(false)
  @Column({type: DataType.STRING})
  password: string;

  @AllowNull(true)
  @Default(UserRole.USER)
  @Column({
    type: DataType.ENUM([UserRole.USER, UserRole.SUPERVISOR, UserRole.ADMIN]),
  })
  role?: string;

  @CreatedAt
  @AllowNull(false)
  @Default(Sequelize.fn('now'))
  @Column({type: DataType.DATE})
  createdAt: Date;

  @UpdatedAt
  @AllowNull(false)
  @Default(Sequelize.fn('now'))
  @Column({type: DataType.DATE})
  updatedAt: Date;
}

export default User;
//item.ts
import {
  Table,
  Model,
  PrimaryKey,
  AllowNull,
  Default,
  Sequelize,
  Column,
  DataType,
} from 'sequelize-typescript';

@Table({modelName: 'items'})
export class Item extends Model<Item> {
  @PrimaryKey
  @AllowNull(false)
  @Default(Sequelize.fn('uuid_generate_v4'))
  @Column({type: DataType.UUID})
  id: string;

  @Column({type: DataType.STRING})
  name: string;

  @Column({type: DataType.STRING})
  description?: string;
}

export default Item;
  • Roles-enum: touch src/core/entities/entity-utils/user-role.enum.ts
export enum UserRole {
  USER = 'User',
  SUPERVISOR = 'Supervisor',
  ADMIN = 'Admin',
}
  • Database connection:
    • touch src/core/sequelize/constants.ts && touch src/core/sequelize/database-provider.ts && touch src/core/sequelize/sequelize-core.module.ts
// constants.ts
export const DB_CONNECTION_TOKEN = 'SequelizeToken';
// database-provider.ts
import { Logger } from '@nestjs/common';

import * as config from 'config';
import { Sequelize } from 'sequelize-typescript';

import { DB_CONNECTION_TOKEN } from './constants';

export const databaseProvider = {
  provide: DB_CONNECTION_TOKEN,
  useFactory: async (): Promise<Sequelize> => {
    const url = config.get('database.url');

    const sequelize = new Sequelize({
      dialect: 'postgres',
      logging: (message: string) => Logger.log(message),
      modelPaths: [
        __dirname + `/../entities/`,
      ],
      url,
    });

    return sequelize;
  },
};
import { Module } from '@nestjs/common';

import { databaseProvider } from './database-provider';

@Module({
  providers: [ databaseProvider ],
  exports: [ databaseProvider ],
})
export class SequelizeCoreModule { }
  • Create Seeds:
    • touch seeds/seed.ts// gets called in the migration.service.ts
import * as faker from 'faker';
import { User } from '../src/core/entities/user';
import { Item } from '../src/core/entities/item';

const generateItems = async () => {
  const items = [];

  for (let i = 0, len = faker.random.number({min: 1, max: 25}); i < len; ++i) {
    const itemData = {
      id: faker.random.uuid(),
      name: faker.name.title(),
      description: faker.random.words(),
    };

    items.push(itemData);
  }

  const itemInstances = await Item.bulkCreate(items);
  return itemInstances;
};

const generateUsers = async () => {
  const defaultUsers = [
    {username: 'LRuebbelke', password: 'password', role: 'Admin'},
    {username: 'JGarvey', password: 'password', role: 'Supervisor'},
    {username: 'VAvila', password: 'password', role: 'User'},
  ];

  const userInstances = await User.bulkCreate(defaultUsers, {ignoreDuplicates: true});
  generateItems();
  return userInstances;
};

export default async () => {
  await generateUsers();
};
  • Authentication:
    • nest g mo core/auth
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';

import * as config from 'config';

import { AuthService } from './auth.service';
import { GqlAuthGuard } from './gql-auth.guard';

@Module({
  imports: [
    JwtModule.register({
      secretOrPrivateKey: config.get('jwt.secret'),
      signOptions: {
        expiresIn: 86400, /// 24 hours/
      },
    }),
  ],
  providers: [AuthService, GqlAuthGuard],
  exports: [AuthService, GqlAuthGuard],
})
export class AuthModule { }
  • nest g s core/auth
import { Injectable } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { User } from '../entities/user';
import { JwtPayload } from './jwt-payload.interface';

interface LoginPayload {
  token: string;
  user: User;
}

@Injectable()
export class AuthService {
  loggedInUser: any;

  constructor(private readonly jwtService: JwtService) {}

  setLoggedInUser(user) {
    this.loggedInUser = user;
  }

  getLoggedInUser() {
    return this.loggedInUser;
  }

  async sign(user): Promise<LoginPayload> {
    const token = await this.jwtService.sign({
      id: user.id,
      username: user.username,
      password: user.password,
      role: user.role,
    });

    return await { token, user };
  }

  async login(userData) {
    const user = await User.findOne({where: {username: userData.username}});

    this.setLoggedInUser(user);

    return this.sign(user);
  }

  async validateUser(payload: JwtPayload): Promise<User> {
    const user = await User.findOne({ where: { id: payload.id } });

    this.setLoggedInUser(user);

    return user;
  }
}
  • touch src/core/auth/gql-auth.guard.ts
import { Injectable, ExecutionContext } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { GqlExecutionContext } from '@nestjs/graphql';

@Injectable()
export class GqlAuthGuard extends AuthGuard('jwt') {
  getRequest(context: ExecutionContext) {
    const ctx = GqlExecutionContext.create(context);

    return ctx.getContext().req;
  }
}
  • touch src/core/auth/jwt-payload.interface.ts
export interface JwtPayload {
  id: string;
  username?: string;
  password?: string;
}
  • GraphQL:
    • Graphql module is setup in the app.module.ts
import { Module, HttpModule, Inject } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';

import { join } from 'path';
import { Sequelize } from 'sequelize-typescript';

import { AppService } from './app.service';
import { AppController } from './app.controller';
import { DB_CONNECTION_TOKEN } from './core/sequelize/constants';
import { MigrationsService } from './core/migrations/migrations.service';
import { SequelizeCoreModule } from './core/sequelize/sequelize-core.module';
import { AuthApiModule } from './auth-api/auth-api.module';
import { ItemApiModule } from './item-api/item-api.module';

@Module({
  imports: [
    HttpModule,
    SequelizeCoreModule,
    ItemApiModule,
    AuthApiModule,
    GraphQLModule.forRoot({
      playground: true,
      context: ({ req }) => ({ req }),
      typePaths: [`${__dirname}/**/*.graphql`],
      definitions: {
        path: join(process.cwd(), 'src/core/entities/__generated/dtos/graphql.schema.ts'),
        outputAs: 'class',
      },
    }),
  ],
  controllers: [
    AppController,
  ],
  providers: [
    AppService,
    MigrationsService,
  ],
})
export class AppModule {
  constructor(
    private migrationsService: MigrationsService,
    @Inject(DB_CONNECTION_TOKEN) private sequelize: Sequelize,
  ) {
    migrationsService.migrateDatabase(sequelize);
  }
}
# .dockerignore
.DS_Store
dist
node_modules
test
FROM* node:10-alpine

ARG NODE_ENV
ENV NODE_ENV $NODE_ENV

WORKDIR /usr/app
EXPOSE 3000

RUN apk add --no-cache \
  bash \
  git \
  postgresql \
  postgresql-contrib \
  python2 \
  python2-dev

COPY package.json .

RUN echo "export PATH=$PATH:/usr/app/node_modules/.bin:" > /etc/environment && yarn

COPY . .

CMD ["npm", "run", "start:dev"]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment