Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
lauti7 authored Apr 2, 2024
0 parents commit baaa1b0
Show file tree
Hide file tree
Showing 28 changed files with 3,093 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
node_modules
npm-debug.log
6 changes: 6 additions & 0 deletions .env
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# This file is supposed to be autogenerated for each environment.
# To set default values for .env please use the file .env.default
# You may want to add this .env file to .gitignore to save development
# credentials

HTTP_SERVER_PORT=5000
15 changes: 15 additions & 0 deletions .env.default
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# This file should be safe to commit and can act as documentation for all
# the possible configurations of our server.

# This file contains the default environment variables, by default,
# it is third in precedence:
# 1. process environment variables
# 2. `.env` file contents
# 3. `.env.default` file contents.

HTTP_SERVER_PORT=3000
HTTP_SERVER_HOST=0.0.0.0

# reset metrics at 00:00UTC
WKC_METRICS_RESET_AT_NIGHT=false

7 changes: 7 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
version: 2
updates:
- package-ecosystem: npm
directory: "/"
schedule:
interval: daily
open-pull-requests-limit: 10
57 changes: 57 additions & 0 deletions .github/workflows/docker.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
name: docker

on:
push:

jobs:
main:
runs-on: ubuntu-latest
steps:
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
# -
# name: Login to DockerHub
# uses: docker/login-action@v1
# with:
# username: ${{ secrets.DOCKERHUB_USERNAME }}
# password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push
id: docker_build
uses: docker/build-push-action@v5
with:
# push: true
tags: well-known-components/template-server:latest
# build-args: |
# arg1=value1
# arg2=value2
- name: Image digest
run: echo ${{ steps.docker_build.outputs.digest }}


# QUAY
# - uses: actions/checkout@v2
# - name: Build Image
# id: build-image
# uses: redhat-actions/buildah-build@v2
# with:
# image: IMAGENAME
# tags: ${{ github.sha }} next
# dockerfiles: |
# ./Dockerfile

# # Podman Login action (https://github.com/redhat-actions/podman-login) also be used to log in,
# # in which case 'username' and 'password' can be omitted.
# - name: Push To quay.io
# id: push-to-quay
# uses: redhat-actions/push-to-registry@v2
# with:
# image: ${{ steps.build-image.outputs.image }}
# tags: ${{ steps.build-image.outputs.tags }}
# registry: quay.io/decentraland
# username: ${{ secrets.QUAY_USERNAME }}
# password: ${{ secrets.QUAY_TOKEN }}

# - name: Print image url
# run: echo "Image pushed to ${{ steps.push-to-quay.outputs.registry-paths }}"
21 changes: 21 additions & 0 deletions .github/workflows/node.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
name: node

on:
push:

jobs:
install:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
- name: Use Node.js 18.x
uses: actions/setup-node@v3
with:
node-version: 18.x
cache: yarn
- name: install
run: yarn
- name: build
run: yarn build
- name: test
run: yarn test
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
dist/
node_modules/
**/.DS_Store
coverage
43 changes: 43 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Launch Program",
"restart": true,
"skipFiles": [
"<node_internals>/**/*.js",
"node_modules/**/*.js"
],
"runtimeExecutable": "node",
"sourceMaps": true,
"showAsyncStacks": true,
"autoAttachChildProcesses": true,
"runtimeArgs": [
"--preserve-symlinks"
],
"outFiles": [
"${workspaceFolder}/dist/**/*.js"
],
"program": "${workspaceFolder}/dist/index.js",
"preLaunchTask": "npm: build"
},
{
"name": "Debug Jest Tests",
"type": "node",
"request": "launch",
"runtimeArgs": [
"--inspect-brk",
"${workspaceRoot}/node_modules/.bin/jest",
"--runInBand"
],
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
"port": 9229
}
]
}
23 changes: 23 additions & 0 deletions .vscode/tasks.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"version": "2.0.0",
"tasks": [
{
"type": "npm",
"script": "build",
"options": {
"env": {
"NODE_ENV": "development"
}
},
"group": {
"kind": "build",
"isDefault": true
},
"problemMatcher": [
"$tsc"
],
"label": "npm: build",
"detail": "build"
}
]
}
44 changes: 44 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
ARG RUN

FROM node:lts as builderenv

WORKDIR /app

# some packages require a build step
RUN apt-get update && apt-get -y -qq install build-essential

# We use Tini to handle signals and PID1 (https://github.com/krallin/tini, read why here https://github.com/krallin/tini/issues/8)
ENV TINI_VERSION v0.19.0
ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini /tini
RUN chmod +x /tini

# install dependencies
COPY package.json /app/package.json
COPY yarn.lock /app/yarn.lock
RUN yarn

# build the app
COPY . /app
RUN yarn build
RUN yarn test

# remove devDependencies, keep only used dependencies
RUN yarn install --frozen-lockfile --production

########################## END OF BUILD STAGE ##########################

FROM node:lts

# NODE_ENV is used to configure some runtime options, like JSON logger
ENV NODE_ENV production

WORKDIR /app
COPY --from=builderenv /app /app
COPY --from=builderenv /tini /tini
# Please _DO NOT_ use a custom ENTRYPOINT because it may prevent signals
# (i.e. SIGTERM) to reach the service
# Read more here: https://aws.amazon.com/blogs/containers/graceful-shutdowns-with-ecs/
# and: https://www.ctl.io/developers/blog/post/gracefully-stopping-docker-containers/
ENTRYPOINT ["/tini", "--"]
# Run the program under Tini
CMD [ "/usr/local/bin/node", "--trace-warnings", "--abort-on-uncaught-exception", "--unhandled-rejections=strict", "dist/index.js" ]
50 changes: 50 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# template-server

## Architecture

Extension of "ports and adapters architecture", also known as "hexagonal architecture".

With this architecture, code is organized into several layers: logic, controllers, adapters, and components (ports).

## Application lifecycle

1. **Start application lifecycle** - Handled by [src/index.ts](src/index.ts) in only one line of code: `Lifecycle.run({ main, initComponents })`
2. **Create components** - Handled by [src/components.ts](src/components.ts) in the function `initComponents`
3. **Wire application & start components** - Handled by [src/service.ts](src/service.ts) in the funciton `main`.
1. First wire HTTP routes and other events with [controllers](#src/controllers)
2. Then call to `startComponents()` to initialize the components (i.e. http-listener)

The same lifecycle is also valid for tests: [test/components.ts](test/components.ts)

## Namespaces

### src/logic

Deals with pure business logic and shouldn't have side-effects or throw exceptions.

### src/controllers

The "glue" between all the other layers, orchestrating calls between pure business logic and adapters.

Controllers always receive an hydrated context containing components and parameters to call the business logic e.g:

```ts
// handler for /ping
export async function pingHandler(context: {
url: URL // parameter added by http-server
components: AppComponents // components of the app, part of the global context
}) {
components.metrics.increment("test_ping_counter")
return { status: 200 }
}
```

### src/adapters

The layer that converts external data representations into internal ones, and vice-versa. Acts as buffer to protect the service from changes in the outside world; when a data representation changes, you only need to change how the adapters deal with it.

### src/components.ts

We use the components abstraction to organize our adapters (e.g. HTTP client, database client, redis client) and any other logic that needs to track mutable state or encode dependencies between stateful components. For every environment (e.g. test, e2e, prod, staging...) we have a different version of our component systems, enabling us to easily inject mocks or different implementations for different contexts.

We make components available to incoming http and kafka handlers. For instance, the http-server handlers have access to things like the database or HTTP components, and pass them down to the controller level for general use.
10 changes: 10 additions & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
module.exports = {
transform: {
"^.+\\.(ts|tsx)$": ["ts-jest", {tsconfig: "test/tsconfig.json"}]
},
moduleFileExtensions: ["ts", "js"],
coverageDirectory: "coverage",
collectCoverageFrom: ["src/**/*.ts", "src/**/*.js"],
testMatch: ["**/*.spec.(ts)"],
testEnvironment: "node",
}
27 changes: 27 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"name": "template-server",
"scripts": {
"build": "tsc -p tsconfig.json",
"start": "node --trace-warnings --abort-on-uncaught-exception --unhandled-rejections=strict dist/index.js",
"test": "jest --forceExit --detectOpenHandles --coverage --verbose"
},
"devDependencies": {
"@types/node": "^20.11.28",
"@well-known-components/test-helpers": "^1.5.6",
"typescript": "^5.4.2"
},
"prettier": {
"printWidth": 120,
"semi": false,
"singleQuote": true,
"trailingComma": "none",
"tabWidth": 2
},
"dependencies": {
"@well-known-components/env-config-provider": "^1.2.0",
"@well-known-components/http-server": "^2.1.0",
"@well-known-components/interfaces": "^1.4.3",
"@well-known-components/logger": "^3.1.3",
"@well-known-components/metrics": "^2.1.0"
}
}
1 change: 1 addition & 0 deletions src/adapters/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Here goes all the code adapters, transforming one data type into another.
29 changes: 29 additions & 0 deletions src/components.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { createDotEnvConfigComponent } from '@well-known-components/env-config-provider'
import {
createServerComponent,
createStatusCheckComponent,
instrumentHttpServerWithPromClientRegistry
} from '@well-known-components/http-server'
import { createLogComponent } from '@well-known-components/logger'
import { createMetricsComponent } from '@well-known-components/metrics'
import { AppComponents, GlobalContext } from './types'
import { metricDeclarations } from './metrics'

// Initialize all the components of the app
export async function initComponents(): Promise<AppComponents> {
const config = await createDotEnvConfigComponent({ path: ['.env.default', '.env'] })
const metrics = await createMetricsComponent(metricDeclarations, { config })
const logs = await createLogComponent({ metrics })
const server = await createServerComponent<GlobalContext>({ config, logs }, {})
const statusChecks = await createStatusCheckComponent({ server, config })

await instrumentHttpServerWithPromClientRegistry({ metrics, server, config, registry: metrics.registry! })

return {
config,
logs,
server,
statusChecks,
metrics
}
}
17 changes: 17 additions & 0 deletions src/controllers/handlers/ping-handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { HandlerContextWithPath } from "../../types"

// handlers arguments only type what they need, to make unit testing easier
export async function pingHandler(context: Pick<HandlerContextWithPath<"metrics", "/ping">, "url" | "components">) {
const {
url,
components: { metrics },
} = context

metrics.increment("test_ping_counter", {
pathname: url.pathname,
})

return {
body: url.pathname,
}
}
12 changes: 12 additions & 0 deletions src/controllers/routes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Router } from "@well-known-components/http-server"
import { GlobalContext } from "../types"
import { pingHandler } from "./handlers/ping-handler"

// We return the entire router because it will be easier to test than a whole server
export async function setupRouter(globalContext: GlobalContext): Promise<Router<GlobalContext>> {
const router = new Router<GlobalContext>()

router.get("/ping", pingHandler)

return router
}
6 changes: 6 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { Lifecycle } from "@well-known-components/interfaces"
import { initComponents } from "./components"
import { main } from "./service"

// This file is the program entry point, it only calls the Lifecycle function
Lifecycle.run({ main, initComponents })
1 change: 1 addition & 0 deletions src/logic/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Here goes all the unit-testable functions.
Loading

0 comments on commit baaa1b0

Please sign in to comment.