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.
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.
- Makefile
- Docker
- editorconfig/prettier
mkdir sparkle-stack
root directorytouch .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:
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.
create-nx-workspace client
cd client
- Generate application:
ng generate app portal
generates application shellng add @angular/material
add Angular Material schematicsng 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 GraphQLng generate lib core-state
state management with NGRXng generate lib graphql
initialize GraphQLng generate lib material
setup Angular Materialng generate lib ui-login
common login componentng 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", "portal"]
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
nest new server
cd server
- Config:
mkdir config
touch config/default.json && config/development.json
- Config:
// 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
- Migrations:
- Sequelize:
'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
- Graphql module is setup in the
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);
}
}
-
- Items-api:
- Auth-api:
- Continue Docker implementations:
touch .dockerignore Dockerfile
# .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"]