Skip to content
Snippets Groups Projects
Verified Commit 3b390721 authored by Louis's avatar Louis :fire:
Browse files

Merge branch 'feature/Modernise' into develop

parents 3b2c1557 e80d6f3c
No related branches found
No related tags found
No related merge requests found
......@@ -2,4 +2,7 @@
.vscode/
node_modules/
.env
.circleci/
\ No newline at end of file
.circleci/
helm/
google-storage.json
*.tgz
\ No newline at end of file
......@@ -4,10 +4,8 @@ root = true
end_of_line = lf
insert_final_new_line = true
charset = utf-8
[*.{rs, js,json,mustache,hbs,handlebars}]
indent_style = tab
[*.{yml,yaml}]
[*.{yaml,yml}]
indent_style = space
indent_size = 2
\ No newline at end of file
APP_NAME=
APP_KEY=
PORT=8000
APP_NAME=jetsam
PORT=10123
WEB_URL=
PROXY_REQUESTS=false
PROXY_HOSTS=
DATABASE_NAME=trash
DATABASE_USER=trash
DATABASE_PASS=trash
API_URL=http://hack.4l2.uk/api
WEB_URL=http://hack.4l2.uk/
MAIL_DRIVER=log
MAIL_FROM_ADDRESS=
MAIL_FROM_NAME=
DATABASE_HOST=127.0.0.1
DATABASE_NAME=jetsam
DATABASE_USER=jetsam
DATABASE_PASS=jetsam
DATABASE_PORT=5434
MAIL_DRIVER=smtp
MAIL_FROM=test@example.com
MAIL_FROM_NAME="Jetsam Tech"
MAIL_REPLY_TO=text@example.com
SMTP_HOST=smtp.mailtrap.io
SMTP_PORT=2525
SMTP_USERNAME=cde41a26e17536
SMTP_PASSWORD=2bb6527a5564d7
SENDGRID_KEY=
FS_DRIVER=gcs
FILE_ROOT=./storage
FILE_URL_PATH=/api/fs
GCS_BUCKET=
GCS_CREDENTIALS_B64=
CACHE_DRIVER=redis
REDIS_URL=redis://127.0.0.1:25000
QUEUE_DRIVER=amqp
SENTRY_DSN=
AMQP_HOST=127.0.0.1
AMQP_PORT=25001
AMQP_USER=guest
AMQP_PASSWORD=guest
AMQP_SECURE=false
SLACK_WEBHOOK=
SENTRY_ENABLED=false
SENTRY_DSN=
\ No newline at end of file
SENTRY_ENABLED=false
SENTRY_SAMPLE_RATE=0
SENTRY_DSN=
module.exports = {
"env": {
"commonjs": true,
"es6": true,
"node": true
},
"extends": "@bark",
"globals": {
"Atomics": "readonly",
"SharedArrayBuffer": "readonly"
},
"parserOptions": {
"ecmaVersion": 2018
},
"rules": {
"indent": 1,
"no-return-await": 0, // Common koa middleware pattern
}
};
\ No newline at end of file
*.png filter=lfs diff=lfs merge=lfs -text
*.jpg filter=lfs diff=lfs merge=lfs -text
*.jpeg filter=lfs diff=lfs merge=lfs -text
......@@ -4,4 +4,13 @@ node_modules
.env
*.iml
**/dist/
google-storage.json
\ No newline at end of file
public/js/
stats.json
client-legacy/
.dck/
storage/
certs/
google-storage.json
*.tgz
.env.formatted
values.yml
\ No newline at end of file
const path = require('path');
const path = require('path')
module.exports = {
'config': path.resolve('src', 'config', 'sequelize.js'),
'models-path': path.resolve('src', 'database', 'models'),
'seeders-path': path.resolve('database', 'seeders'),
'migrations-path': path.resolve('database', 'migrations')
'config': path.resolve('src', 'config', 'sequelize.js'),
'models-path': path.resolve('src', 'database', 'models'),
'seeders-path': path.resolve('database', 'seeders'),
'migrations-path': path.resolve('database', 'migrations'),
}
\ No newline at end of file
const BaseModel = require('./BaseModel')
const timestamps = require('./properties/timestamps')
class Model extends BaseModel {
}
module.exports = (sequelize, DataTypes) => {
const Model = sequelize.define('', Object.assign(
Model.init(Object.assign(
{
id: {
type: DataTypes.UUID,
......@@ -17,27 +22,12 @@ module.exports = (sequelize, DataTypes) => {
},
timestamps(DataTypes),
), {
sequelize,
paranoid: true,
tableName: '',
})
Model.getPolyIdentifier = () => ''
Model.getRelationIdentifier = () => ''
Model.prototype.toJSON = function userToJSON() {
return {
id: this.id,
meta: this.meta,
created_at: this.created_at,
updated_at: this.updated_at,
}
}
Model.associate = function defineModelAssociations(models) {
}
Model.relations = []
return Model
}
\ No newline at end of file
}
module.exports.Model = Model
## [1.3.1] - 2020 / 09 / 04
### Changed
- fromDate is ignored for metric queries, 12 months of data will be returned
\ No newline at end of file
FROM mhart/alpine-node:12
FROM mhart/alpine-node:15 as base
WORKDIR /app
COPY package*.json ./
RUN npm install --only=production --unsafe-perm
RUN npm ci --only=production --unsafe-perm
FROM mhart/alpine-node:slim-12
LABEL maintainer="Louis Capitanchik <contact@louiscap.co>"
LABEL description="A basic Node.js web app"
FROM mhart/alpine-node:slim-15 as api
LABEL maintainer="Louis Capitanchik <louis@jetsam.tech>"
LABEL description="The Jetsam API server"
RUN apk add --no-cache bash
WORKDIR /app
COPY --from=0 /app/node_modules ./node_modules
COPY --from=base /app/node_modules ./node_modules
COPY server.js .
COPY run.js .
COPY .sequelizerc .
COPY src ./src
COPY scripts ./scripts
COPY database ./database
COPY views ./views
# Add more COPY lines here as resources are added
ENV NODE_PATH=/app/src
......@@ -25,4 +31,26 @@ ARG PORT=8000
ENV PORT ${PORT}
EXPOSE ${PORT}
ENV WEB_URL="https://app.jetsam.tech"
ENV APP_KEY=""
ENV DATABASE_HOST=""
ENV DATABASE_NAME=""
ENV DATABASE_USER=""
ENV DATABASE_PASS=""
ENV MAIL_DRIVER=""
ENV MAIL_FROM_ADDRESS=""
ENV MAIL_FROM_NAME=""
ENV SENDGRID_KEY=""
ENV GCS_BUCKET=""
ENV GCS_CREDENTIALS_FILE=""
ENV SENTRY_DSN=""
ENV SLACK_WEBHOOK=""
ENV NODE_ENV="production"
CMD ["node", "server"]
\ No newline at end of file
Makefile 0 → 100644
ORG=jetsam
APPNAME=api
TAG=latest
REMOTE_TAG=$(TAG)
REMOTE=lcr.gr
export
build:
docker build \
--build-arg APP_VERSION=`git rev-parse --short HEAD` \
-t "$(ORG)/$(APPNAME):$(TAG)" --target api .
tag:
docker tag "$(ORG)/$(APPNAME):$(TAG)" "$(REMOTE)/$(ORG)/$(APPNAME):$(REMOTE_TAG)"
push: tag
docker push "$(REMOTE)/$(ORG)/$(APPNAME):$(REMOTE_TAG)"
docker: build tag push
help:
@cat Makefile
# node-koa-starter
A full-fat, go get 'em starter that I use to build enterprise web applications.
## Contents
* [Rationale](#rationale)
* [Included Batteries](#included-batteries)
* [Docker](#docker)
* [Code Conventions](#code-conventions)
* [Dependency Injection](#dependency-injection)
* [Service Facades](#service-facades)
* [Spaces vs Tabs](#spaces-vs-tabs)
## Rationale
I build a lot of large web applications; sometimes they're monoliths, sometimes they're
a series of microservices. This starter represents the collected wisdom that I've gained over
the years on how to structure a Node.js web app so that it's usable both on the micro and macro
scales without too much churn.
The included libraries have been chosen for their utility in any situation; there isn't any point
in including an email facade when not every project would need to send emails (microservices), or
including a JSON:API response transformer (not every project would even speak REST!).
Express is an outdated framework. It has a lot of good things going for it, and the community support
is astounding, but koa is a lot tighter and provides a nicer base on which to build bespoke functionality.
The one controversial point that I will concede is the usage of `NODE_PATH` to get absolute module imports.
This may not be to everyone's taste, but forms a core basis of some of the dependency injection techniques
used.
## Included Batteries
- `koa` - The HTTP request handling framework that forms the core of this starter. Modern, slim, BYOB attitude with enough community modules to cover the bases.
- `koa-static` - Not mounted be default, but included because many projects will need to serve static assets at some point
- `koa-router` - The defacto router for Koa, supports middleware, sub-routers and name based url-generation
- `koa-bodyparser` - Parses request bodies for forms and json uploads
- `koa-logger` - Basic request logging & timing
- `koa-cors` - Handle cors `OPTIONS` requests, very customisable
- `dotenv` - Environment file based configuration, feeds into the `env`/`config` multi-layered app configuration strategy
- `moment` - Almost all apps handle dates or times (even for logging purposes)
- `moment-range` - Configured by default to allow seamless date-range manipulation
- `fs-jetpack` - Easier file system handling. A global instance is added to the `bootstrap` to provide a default fs rooted to the app directory
- `uuid` - Standard for adding IDs to things
- `lodash` - Utilities for most apps
- `core/errors` - HTTP errors that, when not caught, will set the response status and contents appropriately
## Docker
The included `Dockerfile` works by default for this setup, but will need tweaking as new resources are added to the project.
It utilises multi-stage builds to build the app in an alpine container, and then copies the resources to a slim container
for a smaller output size. Additional `COPY` statements and `ENV` configs will need to be added as required.
## Code Conventions
Application entry points are located at the root level of the project. For example, `server.js` is a server entry point. When building
additional components that make use of a shared code base (e.g. a queue worker), they would be located at the root level as well
(e.g. a `worker.js` file).
All external configuration should be stored in a file located under `src/config`. These files should either be a JSON file or a JS
file that exports an object. Using the `config` helper from the `bootstrap` will access values from these files, utilising the node
module cache to prevent multiple potentially expensive lookups (e.g. syscalls to the environment). If the config object has a `driver`
key, the `config` helper will instead use the property with a name that matches the value of `driver` when retrieving values.
e.g.
```javascript
const { env } = require('bootstrap')
module.exports = {
driver: env('DATABASE_DRIVER', 'postgres'),
postgres: {
// ... postgres config ...
},
mysql: {
// ... mysql config ...
},
}
```
External services fall in to two categories; configured SDKs and service facades. Configured SDKs should export a configured version
of their parent-library, or an SDK factory, from `src/vendor/{library-name}` such that another module can `require('vendor/mylib')` to
use that library. An example of this would be using `pg`; `src/vendor/pg` would export a configured `pg` instance, typically using
`config` or `env` from the `bootstrap` to pull in the configuration.
Service facades are detailed more below, but essentially take the form of a configured mapping of a specific vendor service to a
generic service interface. For example, the `cache` service included with this starter has the `null` and `memory` implementations,
which are switched between by the `CACHE_DRIVER` environment variable. A `redis` implementation could be added by mapping an `ioredis`
configured SDK (detailed above) to a `redis` service that implements the `cache` facade.
Dependency injection is detailed more below, but functions by implementing the `ContextualModule` class and then adding that class
to the `ServiceProvider` module. Each `ContextualModule` declares it's exported name, and will be made available to any entity that
has access to the koa `ctx` object.
## Dependency Injection
- TBW
## Service Facades
- TBW
## Spaces Vs Tabs
The project is configured to expect tab-based indentation for the following reasons:
- Accessibility: I am dyslexic and I can configure the width of a tab to render as a comfortable amount of space to make understanding code easier
- Consistency: Every line has X number of tabs based on bracket nesting. There are no spaces for further alignment and the linting rules prevent mixed indentation so it's not possible to succumb to the temptation of vanity indentation
- Preference: I simply prefer tabs. There are arguments for either, and I don't care too deeply about it. Tabs give me the things I need, so I use them.
\ No newline at end of file
version: '3.3'
services:
redis:
image: 'redis:6'
ports:
- '25000:6379'
volumes:
- ./.dck/redis:/data
labels:
uk.co.hackerfest.environment: 'staging'
amqp:
image: 'rabbitmq:3-management'
hostname: jetsam_rabbit
ports:
- '25001:5672'
- '25002:15672'
volumes:
- ./.dck/rabbit:/data
labels:
uk.co.hackerfest.environment: 'staging'
\ No newline at end of file
# Patterns to ignore when building packages.
# This supports shell glob matching, relative path matching, and
# negation (prefixed with !). Only one pattern per line.
.DS_Store
# Common VCS dirs
.git/
.gitignore
.bzr/
.bzrignore
.hg/
.hgignore
.svn/
# Common backup files
*.swp
*.bak
*.tmp
*.orig
*~
# Various IDEs
.project
.idea/
*.tmproj
.vscode/
apiVersion: v2
name: api
description: Jetsam API server
# A chart can be either an 'application' or a 'library' chart.
#
# Application charts are a collection of templates that can be packaged into versioned archives
# to be deployed.
#
# Library charts provide useful utilities or functions for the chart developer. They're included as
# a dependency of application charts to inject those utilities and functions into the rendering
# pipeline. Library charts do not define any templates and therefore cannot be deployed.
type: application
# This is the chart version. This version number should be incremented each time you make changes
# to the chart and its templates, including the app version.
# Versions are expected to follow Semantic Versioning (https://semver.org/)
version: 0.2.8
# This is the version number of the application being deployed. This version number should be
# incremented each time you make changes to the application. Versions are not expected to
# follow Semantic Versioning. They should reflect the version the application is using.
# It is recommended to use it with quotes.
appVersion: "2.0.0.beta-1"
1. Get the application URL by running these commands:
{{- if .Values.ingress.enabled }}
{{- range $host := .Values.ingress.hosts }}
{{- range .paths }}
http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }}
{{- end }}
{{- end }}
{{- else if contains "NodePort" .Values.service.type }}
export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "api.fullname" . }})
export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}")
echo http://$NODE_IP:$NODE_PORT
{{- else if contains "LoadBalancer" .Values.service.type }}
NOTE: It may take a few minutes for the LoadBalancer IP to be available.
You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "api.fullname" . }}'
export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "api.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}")
echo http://$SERVICE_IP:{{ .Values.service.port }}
{{- else if contains "ClusterIP" .Values.service.type }}
export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "api.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}")
export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}")
echo "Visit http://127.0.0.1:8080 to use your application"
kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT
{{- end }}
{{/*
Expand the name of the chart.
*/}}
{{- define "api.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Create a default fully qualified app name.
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
If release name contains chart name it will be used as a full name.
*/}}
{{- define "api.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- if contains $name .Release.Name }}
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{- end }}
{{/*
Create chart name and version as used by the chart label.
*/}}
{{- define "api.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Common labels
*/}}
{{- define "api.labels" -}}
helm.sh/chart: {{ include "api.chart" . }}
{{ include "api.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{/*
Selector labels
*/}}
{{- define "api.selectorLabels" -}}
app.kubernetes.io/name: {{ include "api.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
{{/*
Create the name of the service account to use
*/}}
{{- define "api.serviceAccountName" -}}
{{- if .Values.serviceAccount.create }}
{{- default (include "api.fullname" .) .Values.serviceAccount.name }}
{{- else }}
{{- default "default" .Values.serviceAccount.name }}
{{- end }}
{{- end }}
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "api.fullname" . }}
labels:
{{- include "api.labels" . | nindent 4 }}
spec:
{{- if not .Values.autoscaling.enabled }}
replicas: {{ .Values.server.replicaCount }}
{{- end }}
selector:
matchLabels:
{{- include "api.selectorLabels" . | nindent 6 }}
template:
metadata:
{{- with .Values.podAnnotations }}
annotations:
{{- toYaml . | nindent 8 }}
{{- end }}
labels:
{{- include "api.selectorLabels" . | nindent 8 }}
spec:
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
serviceAccountName: {{ include "api.serviceAccountName" . }}
securityContext:
{{- toYaml .Values.podSecurityContext | nindent 8 }}
initContainers:
- name: init-{{ .Chart.Name }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
env:
{{- toYaml .Values.environment | nindent 12 }}
command:
- node
- /app/scripts/exec-boot.js
- node
- /app/node_modules/.bin/sequelize
- db:migrate
containers:
- name: {{ .Chart.Name }}
securityContext:
{{- toYaml .Values.securityContext | nindent 12 }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
env:
{{- toYaml .Values.environment | nindent 10 }}
- name: GCS_CREDENTIALS_B64
valueFrom:
secretKeyRef:
name: "{{- .Values.secrets.name }}"
key: "key.gcs"
- name: SENTRY_DSN
valueFrom:
secretKeyRef:
name: "{{- .Values.secrets.name }}"
key: "dsn.sentry"
- name: SENDGRID_KEY
valueFrom:
secretKeyRef:
name: "{{- .Values.secrets.name }}"
key: "key.sendgrid"
- name: APP_KEY
valueFrom:
secretKeyRef:
name: "{{- .Values.secrets.name }}"
key: "key.app"
- name: DATABASE_CA_CERT
valueFrom:
secretKeyRef:
name: "{{- .Values.secrets.name }}"
key: "cert.database"
optional: true
ports:
- name: http
containerPort: 80
protocol: TCP
{{- if .Values.server.livenessProbe.enabled }}
livenessProbe:
httpGet:
path: /
port: http
{{- end}}
{{- if .Values.server.readinessProbe.enabled }}
readinessProbe:
httpGet:
path: /
port: http
{{- end}}
resources:
{{- toYaml .Values.resources | nindent 12 }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- if .Values.autoscaling.enabled }}
apiVersion: autoscaling/v2beta1
kind: HorizontalPodAutoscaler
metadata:
name: {{ include "api.fullname" . }}
labels:
{{- include "api.labels" . | nindent 4 }}
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: {{ include "api.fullname" . }}
minReplicas: {{ .Values.autoscaling.minReplicas }}
maxReplicas: {{ .Values.autoscaling.maxReplicas }}
metrics:
{{- if .Values.autoscaling.targetCPUUtilizationPercentage }}
- type: Resource
resource:
name: cpu
targetAverageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }}
{{- end }}
{{- if .Values.autoscaling.targetMemoryUtilizationPercentage }}
- type: Resource
resource:
name: memory
targetAverageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }}
{{- end }}
{{- end }}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment