Bläddra i källkod

add mongodb, context objects, update shutdown code

Aneurin Barker Snook 1 år sedan
förälder
incheckning
f77f77dcc8
8 ändrade filer med 232 tillägg och 10 borttagningar
  1. 15 0
      docker-compose.yml
  2. 129 2
      package-lock.json
  3. 2 1
      package.json
  4. 13 0
      src/account/model.ts
  5. 22 0
      src/db.ts
  6. 6 0
      src/index.ts
  7. 35 7
      src/main.ts
  8. 10 0
      src/types.ts

+ 15 - 0
docker-compose.yml

@@ -0,0 +1,15 @@
+version: "3"
+
+volumes:
+  mongo-data:
+
+services:
+  mongo:
+    image: mongo:7.0.4
+    ports:
+      - "27017:27017"
+    volumes:
+      - mongo-data:/data/db
+    environment:
+      MONGO_INITDB_ROOT_USERNAME: root
+      MONGO_INITDB_ROOT_PASSWORD: root

+ 129 - 2
package-lock.json

@@ -10,7 +10,8 @@
       "license": "SEE LICENSE IN LICENSE.md",
       "dependencies": {
         "dotenv": "^16.3.1",
-        "express": "^4.18.2"
+        "express": "^4.18.2",
+        "mongodb": "^6.3.0"
       },
       "devDependencies": {
         "@types/express": "^4.17.21",
@@ -157,6 +158,14 @@
         "@jridgewell/sourcemap-codec": "^1.4.10"
       }
     },
+    "node_modules/@mongodb-js/saslprep": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.1.1.tgz",
+      "integrity": "sha512-t7c5K033joZZMspnHg/gWPE4kandgc2OxE74aYOtGKfgB9VPuVJPix0H6fhmm2erj5PBJ21mqcx34lpIGtUCsQ==",
+      "dependencies": {
+        "sparse-bitfield": "^3.0.3"
+      }
+    },
     "node_modules/@nodelib/fs.scandir": {
       "version": "2.1.5",
       "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -325,6 +334,19 @@
         "@types/node": "*"
       }
     },
+    "node_modules/@types/webidl-conversions": {
+      "version": "7.0.3",
+      "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz",
+      "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA=="
+    },
+    "node_modules/@types/whatwg-url": {
+      "version": "11.0.3",
+      "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-11.0.3.tgz",
+      "integrity": "sha512-z1ELvMijRL1QmU7QuzDkeYXSF2+dXI0ITKoQsIoVKcNBOiK5RMmWy+pYYxJTHFt8vkpZe7UsvRErQwcxZkjoUw==",
+      "dependencies": {
+        "@types/webidl-conversions": "*"
+      }
+    },
     "node_modules/@typescript-eslint/eslint-plugin": {
       "version": "6.13.2",
       "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.13.2.tgz",
@@ -692,6 +714,14 @@
         "node": ">=8"
       }
     },
+    "node_modules/bson": {
+      "version": "6.2.0",
+      "resolved": "https://registry.npmjs.org/bson/-/bson-6.2.0.tgz",
+      "integrity": "sha512-ID1cI+7bazPDyL9wYy9GaQ8gEEohWvcUl/Yf0dIdutJxnmInEEyCsb4awy/OiBfall7zBA179Pahi3vCdFze3Q==",
+      "engines": {
+        "node": ">=16.20.1"
+      }
+    },
     "node_modules/bytes": {
       "version": "3.1.2",
       "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
@@ -1698,6 +1728,11 @@
         "node": ">= 0.6"
       }
     },
+    "node_modules/memory-pager": {
+      "version": "1.5.0",
+      "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz",
+      "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg=="
+    },
     "node_modules/merge-descriptors": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
@@ -1775,6 +1810,60 @@
         "node": "*"
       }
     },
+    "node_modules/mongodb": {
+      "version": "6.3.0",
+      "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.3.0.tgz",
+      "integrity": "sha512-tt0KuGjGtLUhLoU263+xvQmPHEGTw5LbcNC73EoFRYgSHwZt5tsoJC110hDyO1kjQzpgNrpdcSza9PknWN4LrA==",
+      "dependencies": {
+        "@mongodb-js/saslprep": "^1.1.0",
+        "bson": "^6.2.0",
+        "mongodb-connection-string-url": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=16.20.1"
+      },
+      "peerDependencies": {
+        "@aws-sdk/credential-providers": "^3.188.0",
+        "@mongodb-js/zstd": "^1.1.0",
+        "gcp-metadata": "^5.2.0",
+        "kerberos": "^2.0.1",
+        "mongodb-client-encryption": ">=6.0.0 <7",
+        "snappy": "^7.2.2",
+        "socks": "^2.7.1"
+      },
+      "peerDependenciesMeta": {
+        "@aws-sdk/credential-providers": {
+          "optional": true
+        },
+        "@mongodb-js/zstd": {
+          "optional": true
+        },
+        "gcp-metadata": {
+          "optional": true
+        },
+        "kerberos": {
+          "optional": true
+        },
+        "mongodb-client-encryption": {
+          "optional": true
+        },
+        "snappy": {
+          "optional": true
+        },
+        "socks": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/mongodb-connection-string-url": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.0.tgz",
+      "integrity": "sha512-t1Vf+m1I5hC2M5RJx/7AtxgABy1cZmIPQRMXw+gEIPn/cZNF3Oiy+l0UIypUwVB5trcWHq3crg2g3uAR9aAwsQ==",
+      "dependencies": {
+        "@types/whatwg-url": "^11.0.2",
+        "whatwg-url": "^13.0.0"
+      }
+    },
     "node_modules/ms": {
       "version": "2.1.2",
       "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
@@ -1968,7 +2057,6 @@
       "version": "2.3.1",
       "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
       "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
-      "dev": true,
       "engines": {
         "node": ">=6"
       }
@@ -2242,6 +2330,14 @@
         "node": ">=8"
       }
     },
+    "node_modules/sparse-bitfield": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz",
+      "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==",
+      "dependencies": {
+        "memory-pager": "^1.0.2"
+      }
+    },
     "node_modules/statuses": {
       "version": "2.0.1",
       "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
@@ -2312,6 +2408,17 @@
         "node": ">=0.6"
       }
     },
+    "node_modules/tr46": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/tr46/-/tr46-4.1.1.tgz",
+      "integrity": "sha512-2lv/66T7e5yNyhAAC4NaKe5nVavzuGJQVVtRYLyQ2OI8tsJ61PMLlelehb0wi2Hx6+hT/OJUWZcw8MjlSRnxvw==",
+      "dependencies": {
+        "punycode": "^2.3.0"
+      },
+      "engines": {
+        "node": ">=14"
+      }
+    },
     "node_modules/ts-api-utils": {
       "version": "1.0.3",
       "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.3.tgz",
@@ -2461,6 +2568,26 @@
         "node": ">= 0.8"
       }
     },
+    "node_modules/webidl-conversions": {
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
+      "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/whatwg-url": {
+      "version": "13.0.0",
+      "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-13.0.0.tgz",
+      "integrity": "sha512-9WWbymnqj57+XEuqADHrCJ2eSXzn8WXIW/YSGaZtb2WKAInQ6CHfaUUcTyyver0p8BDg5StLQq8h1vtZuwmOig==",
+      "dependencies": {
+        "tr46": "^4.1.1",
+        "webidl-conversions": "^7.0.0"
+      },
+      "engines": {
+        "node": ">=16"
+      }
+    },
     "node_modules/which": {
       "version": "2.0.2",
       "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",

+ 2 - 1
package.json

@@ -24,6 +24,7 @@
   },
   "dependencies": {
     "dotenv": "^16.3.1",
-    "express": "^4.18.2"
+    "express": "^4.18.2",
+    "mongodb": "^6.3.0"
   }
 }

+ 13 - 0
src/account/model.ts

@@ -0,0 +1,13 @@
+import type { Context } from '../types'
+
+export type AccountModel = ReturnType<typeof createAccountModel>
+
+function createAccountModel(ctx: Context) {
+  const collection = ctx.db.collection('account')
+
+  return {
+    collection,
+  }
+}
+
+export default createAccountModel

+ 22 - 0
src/db.ts

@@ -0,0 +1,22 @@
+import type { AccountModel } from './account/model'
+import type { Context } from './types'
+import { MongoClient } from 'mongodb'
+import createAccountModel from './account/model'
+
+export interface Models {
+  account: AccountModel
+}
+
+async function createDatabase(ctx: Context) {
+  const mongo = await MongoClient.connect(ctx.config.mongo.uri)
+  const db = mongo.db(ctx.config.mongo.db)
+
+  const dbCtx = { ...ctx, mongo, db }
+  const model = <Models>{
+    account: createAccountModel(dbCtx),
+  }
+
+  return { mongo, db, model }
+}
+
+export default createDatabase

+ 6 - 0
src/index.ts

@@ -14,6 +14,12 @@ main({
   log: {
     level: process.env.LOG_LEVEL || 'info',
   },
+  mongo: {
+    db: process.env.MONGO_DB || 'herda',
+    uri: process.env.MONGO_URI || 'mongodb://root:root@localhost:27017',
+  },
+  shutdownTimeout: parseInt(process.env.SHUTDOWN_TIMEOUT || '60000'),
 }).catch(err => {
   if (err) console.error(err)
+  process.exit(1)
 })

+ 35 - 7
src/main.ts

@@ -1,4 +1,5 @@
 import type { SignalConstants } from 'os'
+import createDatabase from './db'
 import { createExpress } from './http'
 import createLogger from './log'
 import process from 'process'
@@ -12,37 +13,64 @@ async function main(config: Config): Promise<void> {
   const ctx = <Context>{ config }
 
   // Initialize logger
-  ctx.log = createLogger(ctx)
+  const log = createLogger(ctx)
+  ctx.log = log
+
+  // Initialize database connection
+  const { mongo, db, model } = await createDatabase(ctx)
+  ctx.mongo = mongo
+  ctx.db = db
+  ctx.model = model
+  log.info('Connected to MongoDB', config.mongo)
 
   // Initialize Express app
   const app = createExpress(ctx)
 
   // Start processes.
   // This promise can only be rejected, signifying that the app has stopped
+  log.info('Starting app')
   return new Promise((res, rej) => {
     // Start HTTP server
     const server = app.listen(config.http.port, config.http.host)
+    log.info('Listening for HTTP connections', config.http)
+
+    // Shutdown function
+    async function stop(e?: keyof SignalConstants) {
+      // Force shutdown if any task hangs
+      const t = setTimeout(() => {
+        rej(new Error(`Waited ${config.shutdownTimeout}ms to shut down, forcing exit`))
+      }, config.shutdownTimeout)
 
-    // Shut down on interrupt or terminate
-    async function stop(e: keyof SignalConstants) {
       await Promise.all([
-        // Stop server
+        // Close database connection
+        (async (): Promise<void> => {
+          try {
+            await mongo.close()
+            log.info('Closed MongoDB connection')
+          } catch (err) {
+            log.error('Failed to close MongoDB connection', err)
+          }
+        })(),
+        // Stop HTTP server
         new Promise<void>((res, rej) => {
           server.close(err => {
             if (err) {
-              ctx.log.error(err)
+              log.error('Failed to stop HTTP server', err)
               rej(err)
             } else {
-              ctx.log.info('Stopped HTTP server')
+              log.info('Stopped HTTP server')
               res()
             }
           })
         }),
       ])
+      clearTimeout(t)
 
-      ctx.log.error(`Received ${e}`)
+      if (e) log.error(`Received ${e}`)
       rej()
     }
+
+    // Shut down on interrupt or terminate
     process.on('SIGINT', e => stop(e).catch(rej))
     process.on('SIGTERM', e => stop(e).catch(rej))
   })

+ 10 - 0
src/types.ts

@@ -1,4 +1,6 @@
 import type { Logger } from './log'
+import type { Models } from './db'
+import type { Db, MongoClient } from 'mongodb'
 
 export interface Config {
   api: {
@@ -11,9 +13,17 @@ export interface Config {
   log: {
     level: string
   }
+  mongo: {
+    db: string
+    uri: string
+  }
+  shutdownTimeout: number
 }
 
 export interface Context {
   config: Config
+  db: Db
   log: Logger
+  model: Models
+  mongo: MongoClient
 }