From 44bdbd3706cb8ae6a5f7d43d7e3b328548d526c9 Mon Sep 17 00:00:00 2001 From: Gunnar Fernqvist Date: Fri, 29 Nov 2024 09:06:21 +0100 Subject: [PATCH 1/8] feat: updated database connection for use with CouchDB --- package-lock.json | 359 +++++++++++++++++++++++++++++++++++++++++++--- package.json | 1 + readme.md | 11 +- src/db_manager.ts | 139 +++++++++++++----- 4 files changed, 451 insertions(+), 59 deletions(-) diff --git a/package-lock.json b/package-lock.json index ad5de0d..7636c36 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "dotenv": "^16.3.1", "fastify": "4.23.2", "mongodb": "^6.5.0", + "nano": "^10.1.4", "nodemon": "^2.0.20", "sdp-transform": "^2.14.1", "ts-node": "^10.9.1", @@ -2283,6 +2284,12 @@ "node": ">=0.10.0" } }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, "node_modules/atomic-sleep": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", @@ -2301,6 +2308,17 @@ "fastq": "^1.6.1" } }, + "node_modules/axios": { + "version": "1.7.7", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz", + "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/babel-jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", @@ -2459,11 +2477,12 @@ } }, "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "license": "MIT", "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" @@ -2559,6 +2578,25 @@ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "dev": true }, + "node_modules/call-bind": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -2745,6 +2783,18 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/compare-func": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/compare-func/-/compare-func-2.0.0.tgz", @@ -2820,9 +2870,10 @@ "dev": true }, "node_modules/cookie": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", - "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -2996,6 +3047,32 @@ "node": ">=0.10.0" } }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -3110,6 +3187,27 @@ "is-arrayish": "^0.2.1" } }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/escalade": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", @@ -3604,9 +3702,10 @@ } }, "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" }, @@ -3663,6 +3762,40 @@ "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==", "dev": true }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", + "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -3707,7 +3840,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -3730,6 +3862,25 @@ "node": "6.* || 8.* || >= 10.*" } }, + "node_modules/get-intrinsic": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/get-package-type": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", @@ -3866,6 +4017,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -3896,11 +4059,46 @@ "node": ">=8" } }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/hasown": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", - "dev": true, "dependencies": { "function-bind": "^1.1.2" }, @@ -4176,6 +4374,7 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "license": "MIT", "engines": { "node": ">=0.12.0" } @@ -5074,15 +5273,22 @@ } }, "node_modules/light-my-request": { - "version": "5.11.0", - "resolved": "https://registry.npmjs.org/light-my-request/-/light-my-request-5.11.0.tgz", - "integrity": "sha512-qkFCeloXCOMpmEdZ/MV91P8AT4fjwFXWaAFz3lUeStM8RcoM1ks4J/F8r1b3r6y/H4u3ACEJ1T+Gv5bopj7oDA==", + "version": "5.14.0", + "resolved": "https://registry.npmjs.org/light-my-request/-/light-my-request-5.14.0.tgz", + "integrity": "sha512-aORPWntbpH5esaYpGOOmri0OHDOe3wC5M2MQxZ9dvMLZm6DnaAn0kJlcbU9hwsQgLzmZyReKwFwwPkR+nHu5kA==", + "license": "BSD-3-Clause", "dependencies": { - "cookie": "^0.5.0", - "process-warning": "^2.0.0", + "cookie": "^0.7.0", + "process-warning": "^3.0.0", "set-cookie-parser": "^2.4.1" } }, + "node_modules/light-my-request/node_modules/process-warning": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-3.0.0.tgz", + "integrity": "sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ==", + "license": "MIT" + }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", @@ -5284,12 +5490,13 @@ } }, "node_modules/micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, + "license": "MIT", "dependencies": { - "braces": "^3.0.2", + "braces": "^3.0.3", "picomatch": "^2.3.1" }, "engines": { @@ -5307,6 +5514,27 @@ "node": ">=10.0.0" } }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/mimic-fn": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", @@ -5426,6 +5654,20 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, + "node_modules/nano": { + "version": "10.1.4", + "resolved": "https://registry.npmjs.org/nano/-/nano-10.1.4.tgz", + "integrity": "sha512-bJOFIPLExIbF6mljnfExXX9Cub4W0puhDjVMp+qV40xl/DBvgKao7St4+6/GB6EoHZap7eFnrnx4mnp5KYgwJA==", + "license": "Apache-2.0", + "dependencies": { + "axios": "^1.7.4", + "node-abort-controller": "^3.1.1", + "qs": "^6.13.0" + }, + "engines": { + "node": ">=14" + } + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -5438,6 +5680,12 @@ "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==", "dev": true }, + "node_modules/node-abort-controller": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz", + "integrity": "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==", + "license": "MIT" + }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -5561,6 +5809,18 @@ "node": ">=8" } }, + "node_modules/object-inspect": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", + "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/obliterator": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/obliterator/-/obliterator-2.0.4.tgz", @@ -5962,6 +6222,12 @@ "node": ">= 0.10" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, "node_modules/pstree.remy": { "version": "1.1.8", "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", @@ -5991,6 +6257,21 @@ } ] }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -6446,6 +6727,23 @@ "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.6.0.tgz", "integrity": "sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ==" }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -6472,6 +6770,24 @@ "node": ">=8" } }, + "node_modules/side-channel": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/signal-exit": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", @@ -6855,6 +7171,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "license": "MIT", "dependencies": { "is-number": "^7.0.0" }, diff --git a/package.json b/package.json index f1979c9..a7715fe 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "dotenv": "^16.3.1", "fastify": "4.23.2", "mongodb": "^6.5.0", + "nano": "^10.1.4", "nodemon": "^2.0.20", "sdp-transform": "^2.14.1", "ts-node": "^10.9.1", diff --git a/readme.md b/readme.md index 52acefe..8988d08 100644 --- a/readme.md +++ b/readme.md @@ -9,7 +9,7 @@ Intercom solution powered by Symphony Media Bridge. This is the Intercom manager ## Requirements - A Symphony Media Bridge running and reachable -- A MongoDB server +- A MongoDB server or CouchDB server - Docker engine ## Environment variables @@ -19,7 +19,8 @@ Intercom solution powered by Symphony Media Bridge. This is the Intercom manager | `PORT` | Intercom-Manager API port | | `SMB_ADDRESS` | The address:port of the Symphony Media Bridge instance | | `SMB_APIKEY` | When set, provide this API key for the Symphony Media Bridge (optional) | -| `MONGODB_CONNECTION_STRING` | MongoDB connection string (default: `mongodb://localhost:27017/intercom-manager`) | +| `DB_CONNECTION_STRING` | DB connection string (default: `mongodb://localhost:27017/intercom-manager`) | +| `MONGODB_CONNECTION_STRING` | DEPRECATED: MongoDB connection string | ## Installation / Usage @@ -29,7 +30,7 @@ Start an Intercom Manager instance: docker run -d -p 8000:8000 \ -e PORT=8000 \ -e SMB_ADDRESS=http://: \ - -e MONGODB_CONNECTION_STRING=mongodb://:/ \ + -e DB_CONNECTION_STRING=://:/ \ eyevinntechnology/intercom-manager ``` @@ -37,7 +38,7 @@ The API docs is then available on `http://localhost:8000/api/docs/` ## Development -Requires Node JS engine >= v18 and [MongoDB](https://www.mongodb.com/docs/manual/administration/install-community/) (tested with MongoDB v7). +Requires Node JS engine >= v18 and [MongoDB](https://www.mongodb.com/docs/manual/administration/install-community/) (tested with MongoDB v7) or [CouchDB](https://docs.couchdb.org/en/stable/index.html). Install dependencies @@ -57,7 +58,7 @@ Start server locally SMB_ADDRESS=http://: SMB_APIKEY= npm start ``` -See [Environment Variables](#environment-variables) for a full list of environment variables you can set. The default `MONGODB_CONNECTION_STRING` is probably what you want to use for local development unless you use a remote db server. +See [Environment Variables](#environment-variables) for a full list of environment variables you can set. The default `DB_CONNECTION_STRING` is probably what you want to use for local development unless you use a remote db server. ## Terraform infrastructure diff --git a/src/db_manager.ts b/src/db_manager.ts index e3e3766..7668327 100644 --- a/src/db_manager.ts +++ b/src/db_manager.ts @@ -1,78 +1,144 @@ import { MongoClient } from 'mongodb'; import { Line, Production } from './models'; import { assert } from './utils'; +import Nano from 'nano'; -const MONGODB_CONNECTION_STRING: string = +const DB_CONNECTION_STRING: string = + process.env.DB_CONNECTION_STRING ?? process.env.MONGODB_CONNECTION_STRING ?? 'mongodb://localhost:27017/intercom-manager'; -const client = new MongoClient(MONGODB_CONNECTION_STRING); -const db = client.db(); +const dbProtocol = DB_CONNECTION_STRING.split(':')[0]; + +const mongoClient = dbProtocol === 'mongodb' ? new MongoClient(DB_CONNECTION_STRING) : null; +const mongoDb = dbProtocol === 'mongodb' ? client.db() : null; +const nanoDb = dbProtocol === 'mongodb' ? null : Nano(DB_CONNECTION_STRING); async function getNextSequence(collectionName: string): Promise { - const ret = await db.command({ - findAndModify: 'counters', - query: { _id: collectionName }, - update: { $inc: { seq: 1 } }, - new: true, - upsert: true - }); - return ret.value.seq; + if (dbProtocol === 'mongodb') { + const ret = await mongoDb.command({ + findAndModify: 'counters', + query: { _id: collectionName }, + update: { $inc: { seq: 1 } }, + new: true, + upsert: true + }); + return ret.value.seq; + } else { + const counterDocId = `counter_${collectionName}`; + let counterDoc; + + try { + counterDoc = await nanoDb.get(counterDocId); + counterDoc.value = (parseInt(counterDoc.value) + 1).toString(); + } catch (error) { +// assert.strictEqual(error.statusCode, 404, 'Unexpected error getting counter document'); + counterDoc = { _id: counterDocId, value: '1' }; + } + await nanoDb.insert(counterDoc); + return counterDoc.value; + } } const dbManager = { async connect(): Promise { - await client.connect(); + if (dbProtocol === 'mongodb') + await mongoClient.connect(); }, async disconnect(): Promise { - await client.close(); + if (dbProtocol === 'mongodb') + await mongoClient.close(); }, /** Get all productions from the database in reverse natural order, limited by the limit parameter */ async getProductions(limit: number, offset: number): Promise { - const productions = await db - .collection('productions') - .find() - .sort({ $natural: -1 }) - .skip(offset) - .limit(limit) - .toArray(); + let productions: Production[] = []; + if (dbProtocol === 'mongodb') { + productions = await mongoDb + .collection('productions') + .find() + .sort({ $natural: -1 }) + .skip(offset) + .limit(limit) + .toArray(); + } else { + const response = await nanoDb.list({ + limit: limit, + skip: offset, + sort: [{ 'name': 'desc' }], + include_docs: true + }); + response.rows.forEach(row => { + if (row.doc._id.toLowerCase().indexOf("counter") === -1) + productions.push(row.doc); + }); + } return productions as any as Production[]; }, async getProductionsLength(): Promise { - return await db.collection('productions').countDocuments(); + if (dbProtocol === 'mongodb') { + return await mongoDb.collection('productions').countDocuments(); + } else { + const productions = await nanoDb.list({ include_docs: false }); + return productions.rows.length; + } }, async getProduction(id: number): Promise { - return db.collection('productions').findOne({ _id: id as any }) as - | any - | undefined; + if (dbProtocol === 'mongodb') { + return mongoDb.collection('productions').findOne({ _id: id as any }) as any | undefined; + } else { + const production = await nanoDb.get(id.toString()); + return production as any | undefined; + } }, async updateProduction( production: Production ): Promise { - const result = await db + if (dbProtocol === 'mongodb') { + const result = await mongoDb .collection('productions') .updateOne({ _id: production._id as any }, { $set: production }); - return result.modifiedCount === 1 ? production : undefined; + return result.modifiedCount === 1 ? production : undefined; + } else { + const existingProduction = await nanoDb.get(production._id.toString()); + const updatedProduction = { ...existingProduction, ...production }; + const response = await nanoDb.insert(updatedProduction); + return response.ok ? production : undefined; + } }, async addProduction(name: string, lines: Line[]): Promise { const _id = await getNextSequence('productions'); + if (_id === -1) { + throw new Error('Failed to get next sequence'); + } const production = { name, lines, _id }; - await db.collection('productions').insertOne(production as any); - return production; + if (dbProtocol === 'mongodb') { + await mongoDb.collection('productions').insertOne(production as any); + return production; + } else { + const production = { name, lines, _id }; + const response = await nanoDb.insert(production); + return response.ok ? production : undefined; + } }, async deleteProduction(productionId: number): Promise { - const result = await db - .collection('productions') - .deleteOne({ _id: productionId as any }); - return result.deletedCount === 1; + if (dbProtocol === 'mongodb') { + const result = await mongoDb + .collection('productions') + .deleteOne({ _id: productionId as any }); + return result.deletedCount === 1; + } else { + const production = await nanoDb.get(productionId.toString()); + const response = await nanoDb.destroy(production._id, production._rev); + return response.ok; + } }, async setLineConferenceId( @@ -88,12 +154,19 @@ const dbManager = { `Line with id "${lineId}" does not exist for production with id "${productionId}"` ); line.smbConferenceId = conferenceId; - await db + if (dbProtocol === 'mongodb') { + await mongoDb .collection('productions') .updateOne( { _id: productionId as any }, { $set: { lines: production.lines } } ); + } else { + const existingProduction = await nanoDb.get(productionId.toString()); + const updatedProduction = { ...existingProduction, lines: production.lines }; + const response = await nanoDb.insert(updatedProduction); + assert(response.ok, `Failed to update production with id "${productionId}"`); + } } }; From bf14c235b38dce703cf11a96462390fa241849a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Birm=C3=A9?= Date: Fri, 29 Nov 2024 17:34:12 +0100 Subject: [PATCH 2/8] fix: linting issues --- readme.md | 14 ++++++------- src/db_manager.ts | 51 +++++++++++++++++++++++++++-------------------- 2 files changed, 36 insertions(+), 29 deletions(-) diff --git a/readme.md b/readme.md index 8988d08..f8d25aa 100644 --- a/readme.md +++ b/readme.md @@ -14,13 +14,13 @@ Intercom solution powered by Symphony Media Bridge. This is the Intercom manager ## Environment variables -| Variable name | Description | -| --------------------------- | --------------------------------------------------------------------------------- | -| `PORT` | Intercom-Manager API port | -| `SMB_ADDRESS` | The address:port of the Symphony Media Bridge instance | -| `SMB_APIKEY` | When set, provide this API key for the Symphony Media Bridge (optional) | -| `DB_CONNECTION_STRING` | DB connection string (default: `mongodb://localhost:27017/intercom-manager`) | -| `MONGODB_CONNECTION_STRING` | DEPRECATED: MongoDB connection string | +| Variable name | Description | +| --------------------------- | ---------------------------------------------------------------------------- | +| `PORT` | Intercom-Manager API port | +| `SMB_ADDRESS` | The address:port of the Symphony Media Bridge instance | +| `SMB_APIKEY` | When set, provide this API key for the Symphony Media Bridge (optional) | +| `DB_CONNECTION_STRING` | DB connection string (default: `mongodb://localhost:27017/intercom-manager`) | +| `MONGODB_CONNECTION_STRING` | DEPRECATED: MongoDB connection string | ## Installation / Usage diff --git a/src/db_manager.ts b/src/db_manager.ts index 7668327..11d9b30 100644 --- a/src/db_manager.ts +++ b/src/db_manager.ts @@ -10,7 +10,8 @@ const DB_CONNECTION_STRING: string = const dbProtocol = DB_CONNECTION_STRING.split(':')[0]; -const mongoClient = dbProtocol === 'mongodb' ? new MongoClient(DB_CONNECTION_STRING) : null; +const mongoClient = + dbProtocol === 'mongodb' ? new MongoClient(DB_CONNECTION_STRING) : null; const mongoDb = dbProtocol === 'mongodb' ? client.db() : null; const nanoDb = dbProtocol === 'mongodb' ? null : Nano(DB_CONNECTION_STRING); @@ -32,7 +33,7 @@ async function getNextSequence(collectionName: string): Promise { counterDoc = await nanoDb.get(counterDocId); counterDoc.value = (parseInt(counterDoc.value) + 1).toString(); } catch (error) { -// assert.strictEqual(error.statusCode, 404, 'Unexpected error getting counter document'); + // assert.strictEqual(error.statusCode, 404, 'Unexpected error getting counter document'); counterDoc = { _id: counterDocId, value: '1' }; } await nanoDb.insert(counterDoc); @@ -42,13 +43,11 @@ async function getNextSequence(collectionName: string): Promise { const dbManager = { async connect(): Promise { - if (dbProtocol === 'mongodb') - await mongoClient.connect(); + if (dbProtocol === 'mongodb') await mongoClient.connect(); }, async disconnect(): Promise { - if (dbProtocol === 'mongodb') - await mongoClient.close(); + if (dbProtocol === 'mongodb') await mongoClient.close(); }, /** Get all productions from the database in reverse natural order, limited by the limit parameter */ @@ -63,14 +62,14 @@ const dbManager = { .limit(limit) .toArray(); } else { - const response = await nanoDb.list({ + const response = await nanoDb.list({ limit: limit, skip: offset, - sort: [{ 'name': 'desc' }], + sort: [{ name: 'desc' }], include_docs: true }); - response.rows.forEach(row => { - if (row.doc._id.toLowerCase().indexOf("counter") === -1) + response.rows.forEach((row) => { + if (row.doc._id.toLowerCase().indexOf('counter') === -1) productions.push(row.doc); }); } @@ -89,7 +88,9 @@ const dbManager = { async getProduction(id: number): Promise { if (dbProtocol === 'mongodb') { - return mongoDb.collection('productions').findOne({ _id: id as any }) as any | undefined; + return mongoDb.collection('productions').findOne({ _id: id as any }) as + | any + | undefined; } else { const production = await nanoDb.get(id.toString()); return production as any | undefined; @@ -101,8 +102,8 @@ const dbManager = { ): Promise { if (dbProtocol === 'mongodb') { const result = await mongoDb - .collection('productions') - .updateOne({ _id: production._id as any }, { $set: production }); + .collection('productions') + .updateOne({ _id: production._id as any }, { $set: production }); return result.modifiedCount === 1 ? production : undefined; } else { const existingProduction = await nanoDb.get(production._id.toString()); @@ -118,8 +119,8 @@ const dbManager = { throw new Error('Failed to get next sequence'); } const production = { name, lines, _id }; - if (dbProtocol === 'mongodb') { - await mongoDb.collection('productions').insertOne(production as any); + if (dbProtocol === 'mongodb') { + await mongoDb.collection('productions').insertOne(production as any); return production; } else { const production = { name, lines, _id }; @@ -156,16 +157,22 @@ const dbManager = { line.smbConferenceId = conferenceId; if (dbProtocol === 'mongodb') { await mongoDb - .collection('productions') - .updateOne( - { _id: productionId as any }, - { $set: { lines: production.lines } } - ); + .collection('productions') + .updateOne( + { _id: productionId as any }, + { $set: { lines: production.lines } } + ); } else { const existingProduction = await nanoDb.get(productionId.toString()); - const updatedProduction = { ...existingProduction, lines: production.lines }; + const updatedProduction = { + ...existingProduction, + lines: production.lines + }; const response = await nanoDb.insert(updatedProduction); - assert(response.ok, `Failed to update production with id "${productionId}"`); + assert( + response.ok, + `Failed to update production with id "${productionId}"` + ); } } }; From d3c1965fa57c5685d81f871a8bd0bbc48b0652ea Mon Sep 17 00:00:00 2001 From: Gunnar Fernqvist Date: Tue, 3 Dec 2024 08:56:53 +0100 Subject: [PATCH 3/8] feat: almost all typing errors fixed, nano issue remains --- src/db_manager.ts | 54 ++++++++++++++++++++++++----------------------- 1 file changed, 28 insertions(+), 26 deletions(-) diff --git a/src/db_manager.ts b/src/db_manager.ts index 11d9b30..6bb234b 100644 --- a/src/db_manager.ts +++ b/src/db_manager.ts @@ -12,11 +12,11 @@ const dbProtocol = DB_CONNECTION_STRING.split(':')[0]; const mongoClient = dbProtocol === 'mongodb' ? new MongoClient(DB_CONNECTION_STRING) : null; -const mongoDb = dbProtocol === 'mongodb' ? client.db() : null; +const mongoDb = (dbProtocol === 'mongodb' && mongoClient) ? mongoClient.db() : null; const nanoDb = dbProtocol === 'mongodb' ? null : Nano(DB_CONNECTION_STRING); async function getNextSequence(collectionName: string): Promise { - if (dbProtocol === 'mongodb') { + if (dbProtocol === 'mongodb' && mongoDb) { const ret = await mongoDb.command({ findAndModify: 'counters', query: { _id: collectionName }, @@ -25,7 +25,7 @@ async function getNextSequence(collectionName: string): Promise { upsert: true }); return ret.value.seq; - } else { + } else if (nanoDb) { const counterDocId = `counter_${collectionName}`; let counterDoc; @@ -39,73 +39,75 @@ async function getNextSequence(collectionName: string): Promise { await nanoDb.insert(counterDoc); return counterDoc.value; } + return -1; } const dbManager = { async connect(): Promise { - if (dbProtocol === 'mongodb') await mongoClient.connect(); + if (dbProtocol === 'mongodb' && mongoClient) await mongoClient.connect(); }, async disconnect(): Promise { - if (dbProtocol === 'mongodb') await mongoClient.close(); + if (dbProtocol === 'mongodb' && mongoClient) await mongoClient.close(); }, /** Get all productions from the database in reverse natural order, limited by the limit parameter */ async getProductions(limit: number, offset: number): Promise { let productions: Production[] = []; - if (dbProtocol === 'mongodb') { - productions = await mongoDb + if (dbProtocol === 'mongodb' && mongoDb) { + productions = (await mongoDb .collection('productions') .find() .sort({ $natural: -1 }) .skip(offset) .limit(limit) - .toArray(); - } else { + .toArray()) as unknown as Production[]; + } else if (nanoDb) { const response = await nanoDb.list({ limit: limit, skip: offset, sort: [{ name: 'desc' }], include_docs: true }); - response.rows.forEach((row) => { + response.rows.forEach((row: any) => { if (row.doc._id.toLowerCase().indexOf('counter') === -1) productions.push(row.doc); }); } - return productions as any as Production[]; }, async getProductionsLength(): Promise { - if (dbProtocol === 'mongodb') { + if (dbProtocol === 'mongodb' && mongoDb) { return await mongoDb.collection('productions').countDocuments(); - } else { + } else if (nanoDb) { const productions = await nanoDb.list({ include_docs: false }); return productions.rows.length; } + return 0; }, async getProduction(id: number): Promise { - if (dbProtocol === 'mongodb') { + if (dbProtocol === 'mongodb' && mongoDb) { return mongoDb.collection('productions').findOne({ _id: id as any }) as | any | undefined; - } else { + } else if (nanoDb) { const production = await nanoDb.get(id.toString()); return production as any | undefined; } + return undefined; }, async updateProduction( production: Production ): Promise { - if (dbProtocol === 'mongodb') { + if (dbProtocol === 'mongodb' && mongoDb) { const result = await mongoDb .collection('productions') .updateOne({ _id: production._id as any }, { $set: production }); return result.modifiedCount === 1 ? production : undefined; - } else { + } else if (nanoDb) { const existingProduction = await nanoDb.get(production._id.toString()); const updatedProduction = { ...existingProduction, ...production }; const response = await nanoDb.insert(updatedProduction); @@ -119,27 +121,27 @@ const dbManager = { throw new Error('Failed to get next sequence'); } const production = { name, lines, _id }; - if (dbProtocol === 'mongodb') { + if (dbProtocol === 'mongodb' && mongoDb) { await mongoDb.collection('productions').insertOne(production as any); - return production; - } else { - const production = { name, lines, _id }; + } else if (nanoDb) { const response = await nanoDb.insert(production); - return response.ok ? production : undefined; + if (!response.ok) throw new Error('Failed to insert production'); } + return production; }, async deleteProduction(productionId: number): Promise { - if (dbProtocol === 'mongodb') { + if (dbProtocol === 'mongodb' && mongoDb) { const result = await mongoDb .collection('productions') .deleteOne({ _id: productionId as any }); return result.deletedCount === 1; - } else { + } else if (nanoDb) { const production = await nanoDb.get(productionId.toString()); const response = await nanoDb.destroy(production._id, production._rev); return response.ok; } + return false; }, async setLineConferenceId( @@ -155,14 +157,14 @@ const dbManager = { `Line with id "${lineId}" does not exist for production with id "${productionId}"` ); line.smbConferenceId = conferenceId; - if (dbProtocol === 'mongodb') { + if (dbProtocol === 'mongodb' && mongoDb) { await mongoDb .collection('productions') .updateOne( { _id: productionId as any }, { $set: { lines: production.lines } } ); - } else { + } else if (nanoDb) { const existingProduction = await nanoDb.get(productionId.toString()); const updatedProduction = { ...existingProduction, From 17b7cfe3d581a4e937548e6a69c79905e2985751 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Birm=C3=A9?= Date: Tue, 3 Dec 2024 15:20:32 +0100 Subject: [PATCH 4/8] chore: fix prettier issue --- src/db_manager.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/db_manager.ts b/src/db_manager.ts index 6bb234b..3eba588 100644 --- a/src/db_manager.ts +++ b/src/db_manager.ts @@ -12,7 +12,8 @@ const dbProtocol = DB_CONNECTION_STRING.split(':')[0]; const mongoClient = dbProtocol === 'mongodb' ? new MongoClient(DB_CONNECTION_STRING) : null; -const mongoDb = (dbProtocol === 'mongodb' && mongoClient) ? mongoClient.db() : null; +const mongoDb = + dbProtocol === 'mongodb' && mongoClient ? mongoClient.db() : null; const nanoDb = dbProtocol === 'mongodb' ? null : Nano(DB_CONNECTION_STRING); async function getNextSequence(collectionName: string): Promise { From 7887042181ff325b16155554b84e8b9efe552b35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Birm=C3=A9?= Date: Tue, 3 Dec 2024 16:24:01 +0100 Subject: [PATCH 5/8] chore: refactor for better db manager isolation --- src/api_productions.ts | 18 +++- src/db/couchdb.ts | 114 ++++++++++++++++++++ src/db/interface.ts | 17 +++ src/db/mongodb.ts | 106 +++++++++++++++++++ src/db_manager.ts | 183 --------------------------------- src/production_manager.test.ts | 92 +++++++++-------- src/production_manager.ts | 30 +++--- 7 files changed, 321 insertions(+), 239 deletions(-) create mode 100644 src/db/couchdb.ts create mode 100644 src/db/interface.ts create mode 100644 src/db/mongodb.ts delete mode 100644 src/db_manager.ts diff --git a/src/api_productions.ts b/src/api_productions.ts index 6aabc51..18701b5 100644 --- a/src/api_productions.ts +++ b/src/api_productions.ts @@ -24,9 +24,25 @@ import { v4 as uuidv4 } from 'uuid'; import { ConnectionQueue } from './connection_queue'; import { CoreFunctions } from './api_productions_core_functions'; import { Log } from './log'; +import { DbManagerMongoDb } from './db/mongodb'; +import { DbManagerCouchDb } from './db/couchdb'; dotenv.config(); -const productionManager = new ProductionManager(); +const DB_CONNECTION_STRING: string = + process.env.DB_CONNECTION_STRING ?? + process.env.MONGODB_CONNECTION_STRING ?? + 'mongodb://localhost:27017/intercom-manager'; +let dbManager; +const dbUrl = new URL(DB_CONNECTION_STRING); +if (dbUrl.protocol === 'mongodb:') { + dbManager = new DbManagerMongoDb(dbUrl); +} else if (dbUrl.protocol === 'couchdb:') { + dbManager = new DbManagerCouchDb(dbUrl); +} else { + throw new Error('Unsupported database protocol'); +} + +const productionManager = new ProductionManager(dbManager); const connectionQueue = new ConnectionQueue(); const coreFunctions = new CoreFunctions(productionManager, connectionQueue); diff --git a/src/db/couchdb.ts b/src/db/couchdb.ts new file mode 100644 index 0000000..53717ef --- /dev/null +++ b/src/db/couchdb.ts @@ -0,0 +1,114 @@ +import { Line, Production } from '../models'; +import { assert } from '../utils'; +import { DbManager } from './interface'; +import nano from 'nano'; + +export class DbManagerCouchDb implements DbManager { + private client; + private nanoDb; + + constructor(dbConnectionUrl: URL) { + this.client = nano(dbConnectionUrl.toString()); + this.nanoDb = this.client.db.use(dbConnectionUrl.pathname); + } + + async connect(): Promise { + // CouchDB does not require a connection + } + + async disconnect(): Promise { + // CouchDB does not require a disconnection + } + + private async getNextSequence(collectionName: string): Promise { + const counterDocId = `counter_${collectionName}`; + let counterDoc; + + try { + counterDoc = await this.nanoDb.get(counterDocId); + counterDoc.value = (parseInt(counterDoc.value) + 1).toString(); + } catch (error) { + // assert.strictEqual(error.statusCode, 404, 'Unexpected error getting counter document'); + counterDoc = { _id: counterDocId, value: '1' }; + } + await this.nanoDb.insert(counterDoc); + return counterDoc.value; + } + + /** Get all productions from the database in reverse natural order, limited by the limit parameter */ + async getProductions(limit: number, offset: number): Promise { + const productions: Production[] = []; + const response = await this.nanoDb.list({ + limit: limit, + skip: offset, + sort: [{ name: 'desc' }], + include_docs: true + }); + response.rows.forEach((row: any) => { + if (row.doc._id.toLowerCase().indexOf('counter') === -1) + productions.push(row.doc); + }); + return productions as any as Production[]; + } + + async getProductionsLength(): Promise { + const productions = await this.nanoDb.list({ include_docs: false }); + return productions.rows.length; + } + + async getProduction(id: number): Promise { + const production = await this.nanoDb.get(id.toString()); + return production as any | undefined; + } + + async updateProduction( + production: Production + ): Promise { + const existingProduction = await this.nanoDb.get(production._id.toString()); + const updatedProduction = { ...existingProduction, ...production }; + const response = await this.nanoDb.insert(updatedProduction); + return response.ok ? production : undefined; + } + + async addProduction(name: string, lines: Line[]): Promise { + const _id = await this.getNextSequence('productions'); + if (_id === -1) { + throw new Error('Failed to get next sequence'); + } + const production = { name, lines, _id }; + const response = await this.nanoDb.insert(production); + if (!response.ok) throw new Error('Failed to insert production'); + } + + async deleteProduction(productionId: number): Promise { + const production = await this.nanoDb.get(productionId.toString()); + const response = await this.nanoDb.destroy(production._id, production._rev); + return response.ok; + + } + + async setLineConferenceId( + productionId: number, + lineId: string, + conferenceId: string + ): Promise { + const production = await this.getProduction(productionId); + assert(production, `Production with id "${productionId}" does not exist`); + const line = production.lines.find((line) => line.id === lineId); + assert( + line, + `Line with id "${lineId}" does not exist for production with id "${productionId}"` + ); + line.smbConferenceId = conferenceId; + const existingProduction = await this.nanoDb.get(productionId.toString()); + const updatedProduction = { + ...existingProduction, + lines: production.lines + }; + const response = await this.nanoDb.insert(updatedProduction); + assert( + response.ok, + `Failed to update production with id "${productionId}"` + ); + } +} diff --git a/src/db/interface.ts b/src/db/interface.ts new file mode 100644 index 0000000..7fd5a5a --- /dev/null +++ b/src/db/interface.ts @@ -0,0 +1,17 @@ +import { Line, Production } from '../models'; + +export interface DbManager { + connect(): Promise; + disconnect(): Promise; + getProduction(id: number): Promise; + getProductions(limit: number, offset: number): Promise; + getProductionsLength(): Promise; + updateProduction(production: Production): Promise; + addProduction(name: string, lines: Line[]): Promise; + deleteProduction(productionId: number): Promise; + setLineConferenceId( + productionId: number, + lineId: string, + conferenceId: string + ): Promise; +} diff --git a/src/db/mongodb.ts b/src/db/mongodb.ts new file mode 100644 index 0000000..b1389b1 --- /dev/null +++ b/src/db/mongodb.ts @@ -0,0 +1,106 @@ +import { MongoClient } from 'mongodb'; +import { DbManager } from './interface'; +import { Line, Production } from '../models'; +import { assert } from '../utils'; + +export class DbManagerMongoDb implements DbManager { + private client: MongoClient; + + constructor(dbConnectionUrl: URL) { + this.client = new MongoClient(dbConnectionUrl.toString()); + } + + async connect(): Promise { + await this.client.connect(); + } + + async disconnect(): Promise { + await this.client.close(); + } + + private async getNextSequence(collectionName: string): Promise { + const db = this.client.db(); + const ret = await db.command({ + findAndModify: 'counters', + query: { _id: collectionName }, + update: { $inc: { seq: 1 } }, + new: true, + upsert: true + }); + return ret.value.seq; + } + + /** Get all productions from the database in reverse natural order, limited by the limit parameter */ + async getProductions(limit: number, offset: number): Promise { + const db = this.client.db(); + const productions = await db + .collection('productions') + .find() + .sort({ $natural: -1 }) + .skip(offset) + .limit(limit) + .toArray(); + + return productions as any as Production[]; + } + + async getProductionsLength(): Promise { + const db = this.client.db(); + return await db.collection('productions').countDocuments(); + } + + async getProduction(id: number): Promise { + const db = this.client.db(); + return db.collection('productions').findOne({ _id: id as any }) as + | any + | undefined; + } + + async updateProduction( + production: Production + ): Promise { + const db = this.client.db(); + const result = await db + .collection('productions') + .updateOne({ _id: production._id as any }, { $set: production }); + return result.modifiedCount === 1 ? production : undefined; + } + + async addProduction(name: string, lines: Line[]): Promise { + const db = this.client.db(); + const _id = await this.getNextSequence('productions'); + const production = { name, lines, _id }; + await db.collection('productions').insertOne(production as any); + return production; + } + + async deleteProduction(productionId: number): Promise { + const db = this.client.db(); + const result = await db + .collection('productions') + .deleteOne({ _id: productionId as any }); + return result.deletedCount === 1; + } + + async setLineConferenceId( + productionId: number, + lineId: string, + conferenceId: string + ): Promise { + const production = await this.getProduction(productionId); + assert(production, `Production with id "${productionId}" does not exist`); + const line = production.lines.find((line) => line.id === lineId); + assert( + line, + `Line with id "${lineId}" does not exist for production with id "${productionId}"` + ); + line.smbConferenceId = conferenceId; + const db = this.client.db(); + await db + .collection('productions') + .updateOne( + { _id: productionId as any }, + { $set: { lines: production.lines } } + ); + } +} diff --git a/src/db_manager.ts b/src/db_manager.ts deleted file mode 100644 index 3eba588..0000000 --- a/src/db_manager.ts +++ /dev/null @@ -1,183 +0,0 @@ -import { MongoClient } from 'mongodb'; -import { Line, Production } from './models'; -import { assert } from './utils'; -import Nano from 'nano'; - -const DB_CONNECTION_STRING: string = - process.env.DB_CONNECTION_STRING ?? - process.env.MONGODB_CONNECTION_STRING ?? - 'mongodb://localhost:27017/intercom-manager'; - -const dbProtocol = DB_CONNECTION_STRING.split(':')[0]; - -const mongoClient = - dbProtocol === 'mongodb' ? new MongoClient(DB_CONNECTION_STRING) : null; -const mongoDb = - dbProtocol === 'mongodb' && mongoClient ? mongoClient.db() : null; -const nanoDb = dbProtocol === 'mongodb' ? null : Nano(DB_CONNECTION_STRING); - -async function getNextSequence(collectionName: string): Promise { - if (dbProtocol === 'mongodb' && mongoDb) { - const ret = await mongoDb.command({ - findAndModify: 'counters', - query: { _id: collectionName }, - update: { $inc: { seq: 1 } }, - new: true, - upsert: true - }); - return ret.value.seq; - } else if (nanoDb) { - const counterDocId = `counter_${collectionName}`; - let counterDoc; - - try { - counterDoc = await nanoDb.get(counterDocId); - counterDoc.value = (parseInt(counterDoc.value) + 1).toString(); - } catch (error) { - // assert.strictEqual(error.statusCode, 404, 'Unexpected error getting counter document'); - counterDoc = { _id: counterDocId, value: '1' }; - } - await nanoDb.insert(counterDoc); - return counterDoc.value; - } - return -1; -} - -const dbManager = { - async connect(): Promise { - if (dbProtocol === 'mongodb' && mongoClient) await mongoClient.connect(); - }, - - async disconnect(): Promise { - if (dbProtocol === 'mongodb' && mongoClient) await mongoClient.close(); - }, - - /** Get all productions from the database in reverse natural order, limited by the limit parameter */ - async getProductions(limit: number, offset: number): Promise { - let productions: Production[] = []; - if (dbProtocol === 'mongodb' && mongoDb) { - productions = (await mongoDb - .collection('productions') - .find() - .sort({ $natural: -1 }) - .skip(offset) - .limit(limit) - .toArray()) as unknown as Production[]; - } else if (nanoDb) { - const response = await nanoDb.list({ - limit: limit, - skip: offset, - sort: [{ name: 'desc' }], - include_docs: true - }); - response.rows.forEach((row: any) => { - if (row.doc._id.toLowerCase().indexOf('counter') === -1) - productions.push(row.doc); - }); - } - return productions as any as Production[]; - }, - - async getProductionsLength(): Promise { - if (dbProtocol === 'mongodb' && mongoDb) { - return await mongoDb.collection('productions').countDocuments(); - } else if (nanoDb) { - const productions = await nanoDb.list({ include_docs: false }); - return productions.rows.length; - } - return 0; - }, - - async getProduction(id: number): Promise { - if (dbProtocol === 'mongodb' && mongoDb) { - return mongoDb.collection('productions').findOne({ _id: id as any }) as - | any - | undefined; - } else if (nanoDb) { - const production = await nanoDb.get(id.toString()); - return production as any | undefined; - } - return undefined; - }, - - async updateProduction( - production: Production - ): Promise { - if (dbProtocol === 'mongodb' && mongoDb) { - const result = await mongoDb - .collection('productions') - .updateOne({ _id: production._id as any }, { $set: production }); - return result.modifiedCount === 1 ? production : undefined; - } else if (nanoDb) { - const existingProduction = await nanoDb.get(production._id.toString()); - const updatedProduction = { ...existingProduction, ...production }; - const response = await nanoDb.insert(updatedProduction); - return response.ok ? production : undefined; - } - }, - - async addProduction(name: string, lines: Line[]): Promise { - const _id = await getNextSequence('productions'); - if (_id === -1) { - throw new Error('Failed to get next sequence'); - } - const production = { name, lines, _id }; - if (dbProtocol === 'mongodb' && mongoDb) { - await mongoDb.collection('productions').insertOne(production as any); - } else if (nanoDb) { - const response = await nanoDb.insert(production); - if (!response.ok) throw new Error('Failed to insert production'); - } - return production; - }, - - async deleteProduction(productionId: number): Promise { - if (dbProtocol === 'mongodb' && mongoDb) { - const result = await mongoDb - .collection('productions') - .deleteOne({ _id: productionId as any }); - return result.deletedCount === 1; - } else if (nanoDb) { - const production = await nanoDb.get(productionId.toString()); - const response = await nanoDb.destroy(production._id, production._rev); - return response.ok; - } - return false; - }, - - async setLineConferenceId( - productionId: number, - lineId: string, - conferenceId: string - ): Promise { - const production = await this.getProduction(productionId); - assert(production, `Production with id "${productionId}" does not exist`); - const line = production.lines.find((line) => line.id === lineId); - assert( - line, - `Line with id "${lineId}" does not exist for production with id "${productionId}"` - ); - line.smbConferenceId = conferenceId; - if (dbProtocol === 'mongodb' && mongoDb) { - await mongoDb - .collection('productions') - .updateOne( - { _id: productionId as any }, - { $set: { lines: production.lines } } - ); - } else if (nanoDb) { - const existingProduction = await nanoDb.get(productionId.toString()); - const updatedProduction = { - ...existingProduction, - lines: production.lines - }; - const response = await nanoDb.insert(updatedProduction); - assert( - response.ok, - `Failed to update production with id "${productionId}"` - ); - } - } -}; - -export default dbManager; diff --git a/src/production_manager.test.ts b/src/production_manager.test.ts index 5995a0b..3546d79 100644 --- a/src/production_manager.test.ts +++ b/src/production_manager.test.ts @@ -5,7 +5,6 @@ import { SmbEndpointDescription, UserSession } from './models'; -import dbManager from './db_manager'; const newProduction: NewProduction = { name: 'productionname', @@ -96,10 +95,12 @@ beforeEach(() => { describe('production_manager', () => { it('calls the dbManager when you try to create a production', async () => { - const { getProduction } = jest.requireMock('./db_manager'); - getProduction.mockReturnValueOnce(undefined).mockReturnValue(newProduction); + const dbManager = jest.requireMock('./db/interface'); + dbManager.getProduction + .mockReturnValueOnce(undefined) + .mockReturnValue(newProduction); - const productionManagerTest = new ProductionManager(); + const productionManagerTest = new ProductionManager(dbManager); const spyAddProduction = jest.spyOn(dbManager, 'addProduction'); @@ -110,13 +111,13 @@ describe('production_manager', () => { describe('production_manager', () => { it('creating an already existing production throws error', async () => { - const { getProduction } = jest.requireMock('./db_manager'); - getProduction + const dbManager = jest.requireMock('./db/interface'); + dbManager.getProduction .mockReturnValueOnce(undefined) .mockReturnValueOnce(newProduction) .mockReturnValueOnce(existingProduction); - const productionManagerTest = new ProductionManager(); + const productionManagerTest = new ProductionManager(dbManager); await productionManagerTest.createProduction(newProduction); @@ -130,13 +131,13 @@ describe('production_manager', () => { describe('production_manager', () => { it('creates production object then gets entire productions list from class instance', async () => { - const { getProduction, getProductions } = jest.requireMock('./db_manager'); - getProduction.mockReturnValueOnce(undefined); - getProductions + const dbManager = jest.requireMock('./db/interface'); + dbManager.getProduction.mockReturnValueOnce(undefined); + dbManager.getProductions .mockReturnValueOnce([]) .mockReturnValueOnce([existingProduction]); - const productionManagerTest = new ProductionManager(); + const productionManagerTest = new ProductionManager(dbManager); expect(await productionManagerTest.getProductions()).toStrictEqual([]); await productionManagerTest.createProduction(newProduction); @@ -147,10 +148,10 @@ describe('production_manager', () => { describe('production_manager', () => { it('getting non existent production object returns undefined', async () => { - const { getProduction } = jest.requireMock('./db_manager'); - getProduction.mockReturnValueOnce(undefined); + const dbManager = jest.requireMock('./db/interface'); + dbManager.getProduction.mockReturnValueOnce(undefined); - const productionManagerTest = new ProductionManager(); + const productionManagerTest = new ProductionManager(dbManager); const nonExistentProduction = await productionManagerTest.getProduction(-1); expect(nonExistentProduction).toStrictEqual(undefined); @@ -183,15 +184,16 @@ describe('production_manager', () => { ] }; - const { getProduction, getProductions, deleteProduction } = - jest.requireMock('./db_manager'); - getProduction.mockReturnValueOnce(undefined).mockReturnValueOnce(undefined); - getProductions + const dbManager = jest.requireMock('./db/interface'); + dbManager.getProduction + .mockReturnValueOnce(undefined) + .mockReturnValueOnce(undefined); + dbManager.getProductions .mockReturnValueOnce([production1, production2]) .mockReturnValueOnce([production2]); - deleteProduction.mockReturnValueOnce(true); + dbManager.deleteProduction.mockReturnValueOnce(true); - const productionManagerTest = new ProductionManager(); + const productionManagerTest = new ProductionManager(dbManager); expect(await productionManagerTest.getProductions()).toStrictEqual([ production1, @@ -206,12 +208,13 @@ describe('production_manager', () => { describe('production_manager', () => { it('deleting non existent production object returns false', async () => { - const { getProductions, deleteProduction } = - jest.requireMock('./db_manager'); - getProductions.mockReturnValueOnce([]); - deleteProduction.mockReturnValueOnce(true).mockReturnValueOnce(false); + const dbManager = jest.requireMock('./db/interface'); + dbManager.getProductions.mockReturnValueOnce([]); + dbManager.deleteProduction + .mockReturnValueOnce(true) + .mockReturnValueOnce(false); - const productionManagerTest = new ProductionManager(); + const productionManagerTest = new ProductionManager(dbManager); expect(await productionManagerTest.deleteProduction(1)).toStrictEqual(true); expect(await productionManagerTest.getProductions()).toStrictEqual([]); @@ -223,10 +226,10 @@ describe('production_manager', () => { describe('production_manager', () => { it('add an endpoint description to line connections', async () => { - const { getProduction } = jest.requireMock('./db_manager'); - getProduction.mockReturnValueOnce(existingProduction); + const dbManager = jest.requireMock('./db/interface'); + dbManager.getProduction.mockReturnValueOnce(existingProduction); - const productionManagerTest = new ProductionManager(); + const productionManagerTest = new ProductionManager(dbManager); productionManagerTest.createUserSession('1', '1', 'sessionId', 'userName'); productionManagerTest.updateUserEndpoint( @@ -259,15 +262,16 @@ describe('production_manager', () => { }); it('add a line to a production', async () => { - const { getProduction, updateProduction } = - jest.requireMock('./db_manager'); - getProduction.mockReturnValueOnce(structuredClone(existingProduction)); + const dbManager = jest.requireMock('./db/interface'); + dbManager.getProduction.mockReturnValueOnce( + structuredClone(existingProduction) + ); - const productionManagerTest = new ProductionManager(); + const productionManagerTest = new ProductionManager(dbManager); const production = await productionManagerTest.getProduction(1); if (production) { await productionManagerTest.addProductionLine(production, 'newName'); - expect(updateProduction).toHaveBeenLastCalledWith({ + expect(dbManager.updateProduction).toHaveBeenLastCalledWith({ _id: 1, name: 'productionname', lines: [ @@ -287,14 +291,15 @@ describe('production_manager', () => { }); it('remove a line from a production', async () => { - const { getProduction, updateProduction } = - jest.requireMock('./db_manager'); - getProduction.mockReturnValueOnce(structuredClone(existingProduction)); - const productionManagerTest = new ProductionManager(); + const dbManager = jest.requireMock('./db/interface'); + dbManager.getProduction.mockReturnValueOnce( + structuredClone(existingProduction) + ); + const productionManagerTest = new ProductionManager(dbManager); const production = await productionManagerTest.getProduction(1); if (production) { await productionManagerTest.deleteProductionLine(production, '1'); - expect(updateProduction).toHaveBeenLastCalledWith({ + expect(dbManager.updateProduction).toHaveBeenLastCalledWith({ _id: 1, name: 'productionname', lines: [] @@ -303,10 +308,11 @@ describe('production_manager', () => { }); it('change the name of a line in a production', async () => { - const { getProduction, updateProduction } = - jest.requireMock('./db_manager'); - getProduction.mockReturnValueOnce(structuredClone(existingProduction)); - const productionManagerTest = new ProductionManager(); + const dbManager = jest.requireMock('./db/interface'); + dbManager.getProduction.mockReturnValueOnce( + structuredClone(existingProduction) + ); + const productionManagerTest = new ProductionManager(dbManager); const production = await productionManagerTest.getProduction(1); if (production) { await productionManagerTest.updateProductionLine( @@ -314,7 +320,7 @@ describe('production_manager', () => { '1', 'newName' ); - expect(updateProduction).toHaveBeenLastCalledWith({ + expect(dbManager.updateProduction).toHaveBeenLastCalledWith({ _id: 1, name: 'productionname', lines: [ diff --git a/src/production_manager.ts b/src/production_manager.ts index 742650a..311383e 100644 --- a/src/production_manager.ts +++ b/src/production_manager.ts @@ -9,8 +9,8 @@ import { UserSession } from './models'; import { assert } from './utils'; -import dbManager from './db_manager'; import { Log } from './log'; +import { DbManager } from './db/interface'; const SESSION_INACTIVE_THRESHOLD = 60_000; const SESSION_EXPIRED_THRESHOLD = 120_000; @@ -18,14 +18,16 @@ const SESSION_PRUNE_THRESHOLD = 7_200_000; export class ProductionManager extends EventEmitter { private userSessions: Record; + private dbManager: DbManager; - constructor() { + constructor(dbManager: DbManager) { super(); + this.dbManager = dbManager; this.userSessions = {}; } async load(): Promise { - dbManager.connect(); + this.dbManager.connect(); } checkUserStatus(): void { @@ -69,7 +71,7 @@ export class ProductionManager extends EventEmitter { newProductionLines.push(newProductionLine); } - return dbManager.addProduction(newProduction.name, newProductionLines); + return this.dbManager.addProduction(newProduction.name, newProductionLines); } async addProductionLine( @@ -86,7 +88,7 @@ export class ProductionManager extends EventEmitter { smbConferenceId: '' }); - return dbManager.updateProduction(production); + return this.dbManager.updateProduction(production); } async updateProductionLine( @@ -97,7 +99,7 @@ export class ProductionManager extends EventEmitter { const line = production.lines.find((line) => line.id === lineId); if (line) { line.name = lineName; - return dbManager.updateProduction(production); + return this.dbManager.updateProduction(production); } return undefined; } @@ -109,21 +111,21 @@ export class ProductionManager extends EventEmitter { const lineIndex = production.lines.findIndex((line) => line.id === lineId); if (lineIndex !== -1) { production.lines.splice(lineIndex, 1); - return dbManager.updateProduction(production); + return this.dbManager.updateProduction(production); } return undefined; } async getProductions(limit = 0, offset = 0): Promise { - return dbManager.getProductions(limit, offset); + return this.dbManager.getProductions(limit, offset); } async getNumberOfProductions(): Promise { - return dbManager.getProductionsLength(); + return this.dbManager.getProductionsLength(); } async getProduction(id: number): Promise { - return dbManager.getProduction(id); + return this.dbManager.getProduction(id); } async requireProduction(id: number): Promise { @@ -136,7 +138,7 @@ export class ProductionManager extends EventEmitter { * Delete the production from the db and local cache */ async deleteProduction(productionId: number): Promise { - return dbManager.deleteProduction(productionId); + return this.dbManager.deleteProduction(productionId); } async setLineId( @@ -149,7 +151,11 @@ export class ProductionManager extends EventEmitter { const line = this.getLine(matchedProduction.lines, lineId); if (line) { line.smbConferenceId = lineSmbId; - await dbManager.setLineConferenceId(productionId, lineId, lineSmbId); + await this.dbManager.setLineConferenceId( + productionId, + lineId, + lineSmbId + ); return line; } } From 153f917e99726347c7da0fbca81d2dbacef5036d Mon Sep 17 00:00:00 2001 From: Gunnar Fernqvist Date: Wed, 4 Dec 2024 13:49:16 +0100 Subject: [PATCH 6/8] fix: added use of databse to fix typing plus some minor fixes after refactoring db --- src/api_productions.ts | 2 +- src/db/couchdb.ts | 36 +++++++++++++++++++++++------------- 2 files changed, 24 insertions(+), 14 deletions(-) diff --git a/src/api_productions.ts b/src/api_productions.ts index 18701b5..cfba052 100644 --- a/src/api_productions.ts +++ b/src/api_productions.ts @@ -36,7 +36,7 @@ let dbManager; const dbUrl = new URL(DB_CONNECTION_STRING); if (dbUrl.protocol === 'mongodb:') { dbManager = new DbManagerMongoDb(dbUrl); -} else if (dbUrl.protocol === 'couchdb:') { +} else if (dbUrl.protocol === 'http:' || dbUrl.protocol === 'https:') { dbManager = new DbManagerCouchDb(dbUrl); } else { throw new Error('Unsupported database protocol'); diff --git a/src/db/couchdb.ts b/src/db/couchdb.ts index 53717ef..f6543ef 100644 --- a/src/db/couchdb.ts +++ b/src/db/couchdb.ts @@ -8,8 +8,11 @@ export class DbManagerCouchDb implements DbManager { private nanoDb; constructor(dbConnectionUrl: URL) { - this.client = nano(dbConnectionUrl.toString()); - this.nanoDb = this.client.db.use(dbConnectionUrl.pathname); + const server = new URL('/', dbConnectionUrl).toString(); + this.client = nano(server); + this.nanoDb = this.client.db.use( + dbConnectionUrl.pathname.replace(/^\//, '') + ); } async connect(): Promise { @@ -22,26 +25,27 @@ export class DbManagerCouchDb implements DbManager { private async getNextSequence(collectionName: string): Promise { const counterDocId = `counter_${collectionName}`; - let counterDoc; + interface CounterDoc { + _id: string; + _rev?: string; + value: string; + } + let counterDoc: CounterDoc; try { - counterDoc = await this.nanoDb.get(counterDocId); + counterDoc = (await this.nanoDb.get(counterDocId)) as CounterDoc; counterDoc.value = (parseInt(counterDoc.value) + 1).toString(); } catch (error) { - // assert.strictEqual(error.statusCode, 404, 'Unexpected error getting counter document'); counterDoc = { _id: counterDocId, value: '1' }; } await this.nanoDb.insert(counterDoc); - return counterDoc.value; + return parseInt(counterDoc.value, 10); } /** Get all productions from the database in reverse natural order, limited by the limit parameter */ async getProductions(limit: number, offset: number): Promise { const productions: Production[] = []; const response = await this.nanoDb.list({ - limit: limit, - skip: offset, - sort: [{ name: 'desc' }], include_docs: true }); response.rows.forEach((row: any) => { @@ -65,7 +69,11 @@ export class DbManagerCouchDb implements DbManager { production: Production ): Promise { const existingProduction = await this.nanoDb.get(production._id.toString()); - const updatedProduction = { ...existingProduction, ...production }; + const updatedProduction = { + ...existingProduction, + ...production, + _id: production._id.toString() + }; const response = await this.nanoDb.insert(updatedProduction); return response.ok ? production : undefined; } @@ -75,16 +83,18 @@ export class DbManagerCouchDb implements DbManager { if (_id === -1) { throw new Error('Failed to get next sequence'); } - const production = { name, lines, _id }; - const response = await this.nanoDb.insert(production); + const insertProduction = { name, lines, _id: _id.toString() }; + const response = await this.nanoDb.insert( + insertProduction as unknown as nano.MaybeDocument + ); if (!response.ok) throw new Error('Failed to insert production'); + return { name, lines, _id } as Production; } async deleteProduction(productionId: number): Promise { const production = await this.nanoDb.get(productionId.toString()); const response = await this.nanoDb.destroy(production._id, production._rev); return response.ok; - } async setLineConferenceId( From a7b22d6fe7b46ab23058870ad102bcfdd20c4858 Mon Sep 17 00:00:00 2001 From: Gunnar Fernqvist Date: Wed, 4 Dec 2024 13:55:39 +0100 Subject: [PATCH 7/8] fix: unit tests fixed after db refactoring --- src/production_manager.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/production_manager.test.ts b/src/production_manager.test.ts index 3546d79..0598b16 100644 --- a/src/production_manager.test.ts +++ b/src/production_manager.test.ts @@ -81,7 +81,7 @@ const SmbEndpointDescriptionMock: SmbEndpointDescription = { } }; -jest.mock('./db_manager', () => ({ +jest.mock('./db/interface', () => ({ addProduction: jest.fn(), getProduction: jest.fn(), getProductions: jest.fn(), From 440ff8ac9bcbdc53d2f8bf3fe7b566e3a21a16a7 Mon Sep 17 00:00:00 2001 From: Gunnar Fernqvist Date: Wed, 4 Dec 2024 14:00:16 +0100 Subject: [PATCH 8/8] fix: api unit tests fixed after db refactoring --- src/api.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api.test.ts b/src/api.test.ts index 26a2724..4fd54cb 100644 --- a/src/api.test.ts +++ b/src/api.test.ts @@ -1,6 +1,6 @@ import api from './api'; -jest.mock('./db_manager'); +jest.mock('./db/interface'); describe('api', () => { it('responds with hello, world!', async () => {