diff --git a/Dockerfile b/Dockerfile index 4c0dc9fa0..cec2228a8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,11 +5,8 @@ WORKDIR /app # mdbtools RUN apk add --no-cache mdbtools mdbtools-utils -# node-gyp -RUN apk add --no-cache python3 make clang build-base - -# node canvas -RUN apk add --no-cache build-base g++ cairo-dev jpeg-dev pango-dev giflib-dev +# pdftotext +RUN apk add --no-cache poppler-utils # dev watch script RUN apk add --no-cache inotify-tools diff --git a/package-lock.json b/package-lock.json index e49489b10..d1bb1d943 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,6 +23,8 @@ "@capacitor/ios": "5.5.1", "@capawesome/capacitor-file-picker": "^5.3.0", "@elastic/elasticsearch": "~8.10.0", + "@fanoutio/grip": "^3.3.1", + "@fanoutio/serve-grip": "^1.3.1", "@google-cloud/vision": "^4.0.2", "@ionic/angular": "^7.7.3", "@julianpoy/recipe-clipper": "^3.1.0", @@ -37,22 +39,19 @@ "@trpc/server": "^10.43.6", "axios": "^1.6.8", "body-parser": "~1.20.2", - "canvas": "^2.11.2", "commander": "^11.1.0", "cookie-parser": "^1.4.6", "core-js": "^3.33.2", "cors": "^2.8.5", "dayjs": "^1.11.10", "debug": "~4.3.4", - "express": "~4.18.2", - "express-grip": "^1.2.1", + "express": "~4.19.2", "extract-zip": "^2.0.1", "firebase": "^10.8.1", "firebase-admin": "^11.11.0", "fraction.js": "^4.3.7", "fs-extra": "^11.1.1", "google-auth-library": "^9.4.2", - "grip": "^1.5.0", "he": "^1.2.0", "https-proxy-agent": "^7.0.2", "ical-generator": "^6.0.0", @@ -69,7 +68,6 @@ "node-fetch": "^2.6.7", "openai": "^4.24.1", "p-limit": "^3.1.0", - "pdfjs-dist": "^3.11.174", "pdfmake": "^0.2.8", "pg": "8.11.3", "pg-hstore": "^2.3.4", @@ -97,7 +95,7 @@ }, "devDependencies": { "@angular-devkit/architect": "0.1701.0", - "@angular-devkit/build-angular": "17.2.0", + "@angular-devkit/build-angular": "17.3.2", "@angular-devkit/core": "17.1.0", "@angular-devkit/schematics": "17.1.0", "@angular-eslint/builder": "17.2.1", @@ -167,18 +165,19 @@ "version": "1.2.6", "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", + "dev": true, "engines": { "node": ">=0.10.0" } }, "node_modules/@ampproject/remapping": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", - "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", "dev": true, "dependencies": { - "@jridgewell/gen-mapping": "^0.3.0", - "@jridgewell/trace-mapping": "^0.3.9" + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" }, "engines": { "node": ">=6.0.0" @@ -200,71 +199,71 @@ } }, "node_modules/@angular-devkit/build-angular": { - "version": "17.2.0", - "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-17.2.0.tgz", - "integrity": "sha512-zO2YKcRRL3Ck3KZ3Ir/lWlciYIguJd3W9iYICKkeK4whi94y3NhrCy0Iualoo2WP7hE043uKQ0SwtVABft0SgA==", + "version": "17.3.2", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-17.3.2.tgz", + "integrity": "sha512-muPCUyL0uHvRkLH4NLWiccER6P2vCm/Q5DDvqyN4XOzzY3tAHHLrKrpvY87sgd2oNJ6Ci8x7GPNcfzR5KELCnw==", "dev": true, "dependencies": { - "@ampproject/remapping": "2.2.1", - "@angular-devkit/architect": "0.1702.0", - "@angular-devkit/build-webpack": "0.1702.0", - "@angular-devkit/core": "17.2.0", - "@babel/core": "7.23.9", + "@ampproject/remapping": "2.3.0", + "@angular-devkit/architect": "0.1703.2", + "@angular-devkit/build-webpack": "0.1703.2", + "@angular-devkit/core": "17.3.2", + "@babel/core": "7.24.0", "@babel/generator": "7.23.6", "@babel/helper-annotate-as-pure": "7.22.5", "@babel/helper-split-export-declaration": "7.22.6", "@babel/plugin-transform-async-generator-functions": "7.23.9", "@babel/plugin-transform-async-to-generator": "7.23.3", - "@babel/plugin-transform-runtime": "7.23.9", - "@babel/preset-env": "7.23.9", - "@babel/runtime": "7.23.9", + "@babel/plugin-transform-runtime": "7.24.0", + "@babel/preset-env": "7.24.0", + "@babel/runtime": "7.24.0", "@discoveryjs/json-ext": "0.5.7", - "@ngtools/webpack": "17.2.0", + "@ngtools/webpack": "17.3.2", "@vitejs/plugin-basic-ssl": "1.1.0", "ansi-colors": "4.1.3", - "autoprefixer": "10.4.17", + "autoprefixer": "10.4.18", "babel-loader": "9.1.3", "babel-plugin-istanbul": "6.1.1", "browserslist": "^4.21.5", "copy-webpack-plugin": "11.0.0", - "critters": "0.0.20", + "critters": "0.0.22", "css-loader": "6.10.0", - "esbuild-wasm": "0.20.0", + "esbuild-wasm": "0.20.1", "fast-glob": "3.3.2", "http-proxy-middleware": "2.0.6", - "https-proxy-agent": "7.0.2", - "inquirer": "9.2.14", + "https-proxy-agent": "7.0.4", + "inquirer": "9.2.15", "jsonc-parser": "3.2.1", "karma-source-map-support": "1.4.0", "less": "4.2.0", "less-loader": "11.1.0", "license-webpack-plugin": "4.0.2", "loader-utils": "3.2.1", - "magic-string": "0.30.7", - "mini-css-extract-plugin": "2.8.0", + "magic-string": "0.30.8", + "mini-css-extract-plugin": "2.8.1", "mrmime": "2.0.0", "open": "8.4.2", "ora": "5.4.1", "parse5-html-rewriting-stream": "7.0.0", "picomatch": "4.0.1", - "piscina": "4.3.1", + "piscina": "4.4.0", "postcss": "8.4.35", - "postcss-loader": "8.1.0", + "postcss-loader": "8.1.1", "resolve-url-loader": "5.0.0", "rxjs": "7.8.1", - "sass": "1.70.0", - "sass-loader": "14.1.0", + "sass": "1.71.1", + "sass-loader": "14.1.1", "semver": "7.6.0", "source-map-loader": "5.0.0", "source-map-support": "0.5.21", - "terser": "5.27.0", + "terser": "5.29.1", "tree-kill": "1.2.2", "tslib": "2.6.2", - "undici": "6.6.2", - "vite": "5.0.12", + "undici": "6.7.1", + "vite": "5.1.5", "watchpack": "2.4.0", - "webpack": "5.90.1", - "webpack-dev-middleware": "6.1.1", + "webpack": "5.90.3", + "webpack-dev-middleware": "6.1.2", "webpack-dev-server": "4.15.1", "webpack-merge": "5.10.0", "webpack-subresource-integrity": "5.1.0" @@ -275,7 +274,7 @@ "yarn": ">= 1.13.0" }, "optionalDependencies": { - "esbuild": "0.20.0" + "esbuild": "0.20.1" }, "peerDependencies": { "@angular/compiler-cli": "^17.0.0", @@ -290,7 +289,7 @@ "ng-packagr": "^17.0.0", "protractor": "^7.0.0", "tailwindcss": "^2.0.0 || ^3.0.0", - "typescript": ">=5.2 <5.4" + "typescript": ">=5.2 <5.5" }, "peerDependenciesMeta": { "@angular/localize": { @@ -329,12 +328,12 @@ } }, "node_modules/@angular-devkit/build-angular/node_modules/@angular-devkit/architect": { - "version": "0.1702.0", - "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1702.0.tgz", - "integrity": "sha512-+HkOYhdq8ez2+yqpxaQ6XtQevOYJNaDpM4oDmZ2lIpiIusFNsmpY2b9iL5PZGb4EfUgN8KsY3n9Q9fmRlRB9eA==", + "version": "0.1703.2", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1703.2.tgz", + "integrity": "sha512-fT5gSzwDHOyGv8zF97t8rjeoYSGSxXjWWstl3rN1nXdO0qgJ5m6Sv0fupON+HltdXDCBLRH+2khNpqx/Fh0Qww==", "dev": true, "dependencies": { - "@angular-devkit/core": "17.2.0", + "@angular-devkit/core": "17.3.2", "rxjs": "7.8.1" }, "engines": { @@ -344,9 +343,9 @@ } }, "node_modules/@angular-devkit/build-angular/node_modules/@angular-devkit/core": { - "version": "17.2.0", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-17.2.0.tgz", - "integrity": "sha512-GIOYHChtDqSOvSiEefJ6hAledEl55J5Pxw8JuKXrM4IJBbviI3c40FAc0Lu5NCj2lYoELOhrLy/UP36sLy+DGA==", + "version": "17.3.2", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-17.3.2.tgz", + "integrity": "sha512-1vxKo9+pdSwTOwqPDSYQh84gZYmCJo6OgR5+AZoGLGMZSeqvi9RG5RiUcOMLQYOnuYv0arlhlWxz0ZjyR8ApKw==", "dev": true, "dependencies": { "ajv": "8.12.0", @@ -371,9 +370,9 @@ } }, "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/linux-x64": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.0.tgz", - "integrity": "sha512-H9Eu6MGse++204XZcYsse1yFHmRXEWgadk2N58O/xd50P9EvFMLJTQLg+lB4E1cF2xhLZU5luSWtGTb0l9UeSg==", + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.1.tgz", + "integrity": "sha512-5gRPk7pKuaIB+tmH+yKd2aQTRpqlf1E4f/mC+tawIm/CGJemZcHZpp2ic8oD83nKgUPMEd0fNanrnFljiruuyA==", "cpu": [ "x64" ], @@ -432,9 +431,9 @@ "dev": true }, "node_modules/@angular-devkit/build-angular/node_modules/esbuild": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.0.tgz", - "integrity": "sha512-6iwE3Y2RVYCME1jLpBqq7LQWK3MW6vjV2bZy6gt/WrqkY+WE74Spyc0ThAOYpMtITvnjX09CrC6ym7A/m9mebA==", + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.1.tgz", + "integrity": "sha512-OJwEgrpWm/PCMsLVWXKqvcjme3bHNpOgN7Tb6cQnR5n0TPbQx1/Xrn7rqM+wn17bYeT6MGB5sn1Bh5YiGi70nA==", "dev": true, "hasInstallScript": true, "optional": true, @@ -445,29 +444,29 @@ "node": ">=12" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.20.0", - "@esbuild/android-arm": "0.20.0", - "@esbuild/android-arm64": "0.20.0", - "@esbuild/android-x64": "0.20.0", - "@esbuild/darwin-arm64": "0.20.0", - "@esbuild/darwin-x64": "0.20.0", - "@esbuild/freebsd-arm64": "0.20.0", - "@esbuild/freebsd-x64": "0.20.0", - "@esbuild/linux-arm": "0.20.0", - "@esbuild/linux-arm64": "0.20.0", - "@esbuild/linux-ia32": "0.20.0", - "@esbuild/linux-loong64": "0.20.0", - "@esbuild/linux-mips64el": "0.20.0", - "@esbuild/linux-ppc64": "0.20.0", - "@esbuild/linux-riscv64": "0.20.0", - "@esbuild/linux-s390x": "0.20.0", - "@esbuild/linux-x64": "0.20.0", - "@esbuild/netbsd-x64": "0.20.0", - "@esbuild/openbsd-x64": "0.20.0", - "@esbuild/sunos-x64": "0.20.0", - "@esbuild/win32-arm64": "0.20.0", - "@esbuild/win32-ia32": "0.20.0", - "@esbuild/win32-x64": "0.20.0" + "@esbuild/aix-ppc64": "0.20.1", + "@esbuild/android-arm": "0.20.1", + "@esbuild/android-arm64": "0.20.1", + "@esbuild/android-x64": "0.20.1", + "@esbuild/darwin-arm64": "0.20.1", + "@esbuild/darwin-x64": "0.20.1", + "@esbuild/freebsd-arm64": "0.20.1", + "@esbuild/freebsd-x64": "0.20.1", + "@esbuild/linux-arm": "0.20.1", + "@esbuild/linux-arm64": "0.20.1", + "@esbuild/linux-ia32": "0.20.1", + "@esbuild/linux-loong64": "0.20.1", + "@esbuild/linux-mips64el": "0.20.1", + "@esbuild/linux-ppc64": "0.20.1", + "@esbuild/linux-riscv64": "0.20.1", + "@esbuild/linux-s390x": "0.20.1", + "@esbuild/linux-x64": "0.20.1", + "@esbuild/netbsd-x64": "0.20.1", + "@esbuild/openbsd-x64": "0.20.1", + "@esbuild/sunos-x64": "0.20.1", + "@esbuild/win32-arm64": "0.20.1", + "@esbuild/win32-ia32": "0.20.1", + "@esbuild/win32-x64": "0.20.1" } }, "node_modules/@angular-devkit/build-angular/node_modules/figures": { @@ -486,9 +485,9 @@ } }, "node_modules/@angular-devkit/build-angular/node_modules/inquirer": { - "version": "9.2.14", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-9.2.14.tgz", - "integrity": "sha512-4ByIMt677Iz5AvjyKrDpzaepIyMewNvDcvwpVVRZNmy9dLakVoVgdCHZXbK1SlVJra1db0JZ6XkJyHsanpdrdQ==", + "version": "9.2.15", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-9.2.15.tgz", + "integrity": "sha512-vI2w4zl/mDluHt9YEQ/543VTCwPKWiHzKtm9dM2V0NdFcqEexDAjUHzO1oA60HRNaVifGXXM1tRRNluLVHa0Kg==", "dev": true, "dependencies": { "@ljharb/through": "^2.3.12", @@ -530,9 +529,9 @@ } }, "node_modules/@angular-devkit/build-angular/node_modules/magic-string": { - "version": "0.30.7", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.7.tgz", - "integrity": "sha512-8vBuFF/I/+OSLRmdf2wwFCJCz+nSn0m6DPvGH1fS/KiQoSaR+sETbov0eIk9KhEKy8CYqIkIAnbohxT/4H0kuA==", + "version": "0.30.8", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.8.tgz", + "integrity": "sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==", "dev": true, "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" @@ -589,12 +588,12 @@ "dev": true }, "node_modules/@angular-devkit/build-webpack": { - "version": "0.1702.0", - "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1702.0.tgz", - "integrity": "sha512-HrJ01MXlXNCeJeohIOIjpulWktUUJQpq01OWX4UazLnN0DAHKIFCwiKZZio5rYIFFUjdKI0+cCGxFbkzetRjWg==", + "version": "0.1703.2", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1703.2.tgz", + "integrity": "sha512-w7rVFQcZK4iTCd/MLfQWIkDkwBOfAs++txNQyS9qYID8KvLs1V+oWYd2qDBRelRv1u3YtaCIS1pQx3GFKBC3OA==", "dev": true, "dependencies": { - "@angular-devkit/architect": "0.1702.0", + "@angular-devkit/architect": "0.1703.2", "rxjs": "7.8.1" }, "engines": { @@ -608,12 +607,12 @@ } }, "node_modules/@angular-devkit/build-webpack/node_modules/@angular-devkit/architect": { - "version": "0.1702.0", - "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1702.0.tgz", - "integrity": "sha512-+HkOYhdq8ez2+yqpxaQ6XtQevOYJNaDpM4oDmZ2lIpiIusFNsmpY2b9iL5PZGb4EfUgN8KsY3n9Q9fmRlRB9eA==", + "version": "0.1703.2", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1703.2.tgz", + "integrity": "sha512-fT5gSzwDHOyGv8zF97t8rjeoYSGSxXjWWstl3rN1nXdO0qgJ5m6Sv0fupON+HltdXDCBLRH+2khNpqx/Fh0Qww==", "dev": true, "dependencies": { - "@angular-devkit/core": "17.2.0", + "@angular-devkit/core": "17.3.2", "rxjs": "7.8.1" }, "engines": { @@ -623,9 +622,9 @@ } }, "node_modules/@angular-devkit/build-webpack/node_modules/@angular-devkit/core": { - "version": "17.2.0", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-17.2.0.tgz", - "integrity": "sha512-GIOYHChtDqSOvSiEefJ6hAledEl55J5Pxw8JuKXrM4IJBbviI3c40FAc0Lu5NCj2lYoELOhrLy/UP36sLy+DGA==", + "version": "17.3.2", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-17.3.2.tgz", + "integrity": "sha512-1vxKo9+pdSwTOwqPDSYQh84gZYmCJo6OgR5+AZoGLGMZSeqvi9RG5RiUcOMLQYOnuYv0arlhlWxz0ZjyR8ApKw==", "dev": true, "dependencies": { "ajv": "8.12.0", @@ -2463,12 +2462,12 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz", - "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==", + "version": "7.24.2", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.2.tgz", + "integrity": "sha512-y5+tLQyV8pg3fsiln67BVLD1P13Eg4lh5RW9mF0zUuvLrv9uIQ4MCL+CRT+FTsBlBjcIan6PGsLcBN0m3ClUyQ==", "dependencies": { - "@babel/highlight": "^7.23.4", - "chalk": "^2.4.2" + "@babel/highlight": "^7.24.2", + "picocolors": "^1.0.0" }, "engines": { "node": ">=6.9.0" @@ -2484,9 +2483,9 @@ } }, "node_modules/@babel/core": { - "version": "7.23.9", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.9.tgz", - "integrity": "sha512-5q0175NOjddqpvvzU+kDiSOAk4PfdO6FvwCWoQ6RO7rTzEe8vlo+4HVfcnAREhD4npMs0e9uZypjTwzZPCf/cw==", + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.0.tgz", + "integrity": "sha512-fQfkg0Gjkza3nf0c7/w6Xf34BW4YvzNfACRLmmb7XRLa6XHdR+K9AlJlxneFfWYf6uhOzuzZVTjF/8KfndZANw==", "dev": true, "dependencies": { "@ampproject/remapping": "^2.2.0", @@ -2494,11 +2493,11 @@ "@babel/generator": "^7.23.6", "@babel/helper-compilation-targets": "^7.23.6", "@babel/helper-module-transforms": "^7.23.3", - "@babel/helpers": "^7.23.9", - "@babel/parser": "^7.23.9", - "@babel/template": "^7.23.9", - "@babel/traverse": "^7.23.9", - "@babel/types": "^7.23.9", + "@babel/helpers": "^7.24.0", + "@babel/parser": "^7.24.0", + "@babel/template": "^7.24.0", + "@babel/traverse": "^7.24.0", + "@babel/types": "^7.24.0", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -2756,9 +2755,9 @@ } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz", - "integrity": "sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==", + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.0.tgz", + "integrity": "sha512-9cUznXMG0+FxRuJfvL82QlTqIzhVW9sL0KjMPHhAOOvpQGL8QtdxnBKILjBqxlHyliz0yCa1G903ZXI/FuHy2w==", "dev": true, "engines": { "node": ">=6.9.0" @@ -2874,36 +2873,37 @@ } }, "node_modules/@babel/helpers": { - "version": "7.23.9", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.9.tgz", - "integrity": "sha512-87ICKgU5t5SzOT7sBMfCOZQ2rHjRU+Pcb9BoILMYz600W6DkVRLFBPwQ18gwUVvggqXivaUakpnxWQGbpywbBQ==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.1.tgz", + "integrity": "sha512-BpU09QqEe6ZCHuIHFphEFgvNSrubve1FtyMton26ekZ85gRGi6LrTF7zArARp2YvyFxloeiRmtSCq5sjh1WqIg==", "dev": true, "dependencies": { - "@babel/template": "^7.23.9", - "@babel/traverse": "^7.23.9", - "@babel/types": "^7.23.9" + "@babel/template": "^7.24.0", + "@babel/traverse": "^7.24.1", + "@babel/types": "^7.24.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/highlight": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz", - "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==", + "version": "7.24.2", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.2.tgz", + "integrity": "sha512-Yac1ao4flkTxTteCDZLEvdxg2fZfz1v8M4QpaGypq/WPDqg3ijHYbDfs+LG5hvzSoqaSZ9/Z9lKSP3CjZjv+pA==", "dependencies": { "@babel/helper-validator-identifier": "^7.22.20", "chalk": "^2.4.2", - "js-tokens": "^4.0.0" + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.23.9", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.9.tgz", - "integrity": "sha512-9tcKgqKbs3xGJ+NtKF2ndOBBLVwPjl1SHxPQkd36r3Dlirw3xWUeGaTbqr7uGZcTaxkVNwc+03SVP7aCdWrTlA==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.1.tgz", + "integrity": "sha512-Zo9c7N3xdOIQrNip7Lc9wvRPzlRtovHVE4lkz8WEDr7uYh/GMQhSiIgFxGIArRHYdJE5kxtZjAf8rT0xhdLCzg==", "bin": { "parser": "bin/babel-parser.js" }, @@ -3751,16 +3751,15 @@ } }, "node_modules/@babel/plugin-transform-object-rest-spread": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.23.4.tgz", - "integrity": "sha512-9x9K1YyeQVw0iOXJlIzwm8ltobIIv7j2iLyP2jIhEbqPRQ7ScNgwQufU2I0Gq11VjyG4gI4yMXt2VFags+1N3g==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.24.1.tgz", + "integrity": "sha512-XjD5f0YqOtebto4HGISLNfiNMTTs6tbkFf2TOqJlYKYmbo+mN9Dnpl4SRoofiziuOWMIyq3sZEUqLo3hLITFEA==", "dev": true, "dependencies": { - "@babel/compat-data": "^7.23.3", - "@babel/helper-compilation-targets": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-compilation-targets": "^7.23.6", + "@babel/helper-plugin-utils": "^7.24.0", "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-transform-parameters": "^7.23.3" + "@babel/plugin-transform-parameters": "^7.24.1" }, "engines": { "node": ">=6.9.0" @@ -3819,12 +3818,12 @@ } }, "node_modules/@babel/plugin-transform-parameters": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.23.3.tgz", - "integrity": "sha512-09lMt6UsUb3/34BbECKVbVwrT9bO6lILWln237z7sLaWnMsTi7Yc9fhX5DLpkJzAGfaReXI22wP41SZmnAA3Vw==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.24.1.tgz", + "integrity": "sha512-8Jl6V24g+Uw5OGPeWNKrKqXPDw2YDjLc53ojwfMcKwlEoETKU9rU0mHUtcg9JntWI/QYzGAXNWEcVHZ+fR+XXg==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.0" }, "engines": { "node": ">=6.9.0" @@ -3914,13 +3913,13 @@ } }, "node_modules/@babel/plugin-transform-runtime": { - "version": "7.23.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.23.9.tgz", - "integrity": "sha512-A7clW3a0aSjm3ONU9o2HAILSegJCYlEZmOhmBRReVtIpY/Z/p7yIZ+wR41Z+UipwdGuqwtID/V/dOdZXjwi9gQ==", + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.24.0.tgz", + "integrity": "sha512-zc0GA5IitLKJrSfXlXmp8KDqLrnGECK7YRfQBmEKg1NmBOQ7e+KuclBEKJgzifQeUYLdNiAw4B4bjyvzWVLiSA==", "dev": true, "dependencies": { "@babel/helper-module-imports": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-plugin-utils": "^7.24.0", "babel-plugin-polyfill-corejs2": "^0.4.8", "babel-plugin-polyfill-corejs3": "^0.9.0", "babel-plugin-polyfill-regenerator": "^0.5.5", @@ -4100,14 +4099,14 @@ } }, "node_modules/@babel/preset-env": { - "version": "7.23.9", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.23.9.tgz", - "integrity": "sha512-3kBGTNBBk9DQiPoXYS0g0BYlwTQYUTifqgKTjxUwEUkduRT2QOa0FPGBJ+NROQhGyYO5BuTJwGvBnqKDykac6A==", + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.24.0.tgz", + "integrity": "sha512-ZxPEzV9IgvGn73iK0E6VB9/95Nd7aMFpbE0l8KQFDG70cOV9IxRP7Y2FUPmlK0v6ImlLqYX50iuZ3ZTVhOF2lA==", "dev": true, "dependencies": { "@babel/compat-data": "^7.23.5", "@babel/helper-compilation-targets": "^7.23.6", - "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-plugin-utils": "^7.24.0", "@babel/helper-validator-option": "^7.23.5", "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.23.3", "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.23.3", @@ -4160,7 +4159,7 @@ "@babel/plugin-transform-new-target": "^7.23.3", "@babel/plugin-transform-nullish-coalescing-operator": "^7.23.4", "@babel/plugin-transform-numeric-separator": "^7.23.4", - "@babel/plugin-transform-object-rest-spread": "^7.23.4", + "@babel/plugin-transform-object-rest-spread": "^7.24.0", "@babel/plugin-transform-object-super": "^7.23.3", "@babel/plugin-transform-optional-catch-binding": "^7.23.4", "@babel/plugin-transform-optional-chaining": "^7.23.4", @@ -4242,9 +4241,9 @@ "dev": true }, "node_modules/@babel/runtime": { - "version": "7.23.9", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.9.tgz", - "integrity": "sha512-0CX6F+BI2s9dkUqr08KFrAIZgNFj75rdBU/DjCyYLIaV/quFjkk6T+EJ2LkZHyZTbEV4L5p97mNkUsHl2wLFAw==", + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.0.tgz", + "integrity": "sha512-Chk32uHMg6TnQdvw2e9IlqPpFX/6NLuK0Ys2PqLb7/gL5uFn9mXvK715FGLlOLQrcO4qIkNHkvPGktzzXexsFw==", "dependencies": { "regenerator-runtime": "^0.14.0" }, @@ -4258,33 +4257,33 @@ "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" }, "node_modules/@babel/template": { - "version": "7.23.9", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.23.9.tgz", - "integrity": "sha512-+xrD2BWLpvHKNmX2QbpdpsBaWnRxahMwJjO+KZk2JOElj5nSmKezyS1B4u+QbHMTX69t4ukm6hh9lsYQ7GHCKA==", + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.0.tgz", + "integrity": "sha512-Bkf2q8lMB0AFpX0NFEqSbx1OkTHf0f+0j82mkw+ZpzBnkk7e9Ql0891vlfgi+kHwOk8tQjiQHpqh4LaSa0fKEA==", "dev": true, "dependencies": { "@babel/code-frame": "^7.23.5", - "@babel/parser": "^7.23.9", - "@babel/types": "^7.23.9" + "@babel/parser": "^7.24.0", + "@babel/types": "^7.24.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.23.9", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.9.tgz", - "integrity": "sha512-I/4UJ9vs90OkBtY6iiiTORVMyIhJ4kAVmsKo9KFc8UOxMeUfi2hvtIBsET5u9GizXE6/GFSuKCTNfgCswuEjRg==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.1.tgz", + "integrity": "sha512-xuU6o9m68KeqZbQuDt2TcKSxUw/mrsvavlEqQ1leZ/B+C9tk6E4sRWy97WaXgvq5E+nU3cXMxv3WKOCanVMCmQ==", "dev": true, "dependencies": { - "@babel/code-frame": "^7.23.5", - "@babel/generator": "^7.23.6", + "@babel/code-frame": "^7.24.1", + "@babel/generator": "^7.24.1", "@babel/helper-environment-visitor": "^7.22.20", "@babel/helper-function-name": "^7.23.0", "@babel/helper-hoist-variables": "^7.22.5", "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/parser": "^7.23.9", - "@babel/types": "^7.23.9", + "@babel/parser": "^7.24.1", + "@babel/types": "^7.24.0", "debug": "^4.3.1", "globals": "^11.1.0" }, @@ -4292,10 +4291,25 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/traverse/node_modules/@babel/generator": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.1.tgz", + "integrity": "sha512-DfCRfZsBcrPEHUfuBMgbJ1Ut01Y/itOs+hY2nFLgqsqXd52/iSiVq5TITtUasIUgm+IIKdY2/1I7auiQOEeC9A==", + "dev": true, + "dependencies": { + "@babel/types": "^7.24.0", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/types": { - "version": "7.23.9", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.9.tgz", - "integrity": "sha512-dQjSq/7HaSjRM43FFGnv5keM2HsxpmyV1PfaSVm0nzzjwwTmjOe6J4bC8e3+pTEIgHaHj+1ZlLThRJ2auc/w1Q==", + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.0.tgz", + "integrity": "sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w==", "dependencies": { "@babel/helper-string-parser": "^7.23.4", "@babel/helper-validator-identifier": "^7.22.20", @@ -4473,9 +4487,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.0.tgz", - "integrity": "sha512-fGFDEctNh0CcSwsiRPxiaqX0P5rq+AqE0SRhYGZ4PX46Lg1FNR6oCxJghf8YgY0WQEgQuh3lErUFE4KxLeRmmw==", + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.1.tgz", + "integrity": "sha512-m55cpeupQ2DbuRGQMMZDzbv9J9PgVelPjlcmM5kxHnrBdBx6REaEd7LamYV7Dm8N7rCyR/XwU6rVP8ploKtIkA==", "cpu": [ "ppc64" ], @@ -4489,9 +4503,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.0.tgz", - "integrity": "sha512-3bMAfInvByLHfJwYPJRlpTeaQA75n8C/QKpEaiS4HrFWFiJlNI0vzq/zCjBrhAYcPyVPG7Eo9dMrcQXuqmNk5g==", + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.1.tgz", + "integrity": "sha512-4j0+G27/2ZXGWR5okcJi7pQYhmkVgb4D7UKwxcqrjhvp5TKWx3cUjgB1CGj1mfdmJBQ9VnUGgUhign+FPF2Zgw==", "cpu": [ "arm" ], @@ -4505,9 +4519,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.0.tgz", - "integrity": "sha512-aVpnM4lURNkp0D3qPoAzSG92VXStYmoVPOgXveAUoQBWRSuQzt51yvSju29J6AHPmwY1BjH49uR29oyfH1ra8Q==", + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.1.tgz", + "integrity": "sha512-hCnXNF0HM6AjowP+Zou0ZJMWWa1VkD77BXe959zERgGJBBxB+sV+J9f/rcjeg2c5bsukD/n17RKWXGFCO5dD5A==", "cpu": [ "arm64" ], @@ -4521,9 +4535,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.0.tgz", - "integrity": "sha512-uK7wAnlRvjkCPzh8jJ+QejFyrP8ObKuR5cBIsQZ+qbMunwR8sbd8krmMbxTLSrDhiPZaJYKQAU5Y3iMDcZPhyQ==", + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.1.tgz", + "integrity": "sha512-MSfZMBoAsnhpS+2yMFYIQUPs8Z19ajwfuaSZx+tSl09xrHZCjbeXXMsUF/0oq7ojxYEpsSo4c0SfjxOYXRbpaA==", "cpu": [ "x64" ], @@ -4537,9 +4551,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.0.tgz", - "integrity": "sha512-AjEcivGAlPs3UAcJedMa9qYg9eSfU6FnGHJjT8s346HSKkrcWlYezGE8VaO2xKfvvlZkgAhyvl06OJOxiMgOYQ==", + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.1.tgz", + "integrity": "sha512-Ylk6rzgMD8klUklGPzS414UQLa5NPXZD5tf8JmQU8GQrj6BrFA/Ic9tb2zRe1kOZyCbGl+e8VMbDRazCEBqPvA==", "cpu": [ "arm64" ], @@ -4553,9 +4567,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.0.tgz", - "integrity": "sha512-bsgTPoyYDnPv8ER0HqnJggXK6RyFy4PH4rtsId0V7Efa90u2+EifxytE9pZnsDgExgkARy24WUQGv9irVbTvIw==", + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.1.tgz", + "integrity": "sha512-pFIfj7U2w5sMp52wTY1XVOdoxw+GDwy9FsK3OFz4BpMAjvZVs0dT1VXs8aQm22nhwoIWUmIRaE+4xow8xfIDZA==", "cpu": [ "x64" ], @@ -4569,9 +4583,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.0.tgz", - "integrity": "sha512-kQ7jYdlKS335mpGbMW5tEe3IrQFIok9r84EM3PXB8qBFJPSc6dpWfrtsC/y1pyrz82xfUIn5ZrnSHQQsd6jebQ==", + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.1.tgz", + "integrity": "sha512-UyW1WZvHDuM4xDz0jWun4qtQFauNdXjXOtIy7SYdf7pbxSWWVlqhnR/T2TpX6LX5NI62spt0a3ldIIEkPM6RHw==", "cpu": [ "arm64" ], @@ -4585,9 +4599,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.0.tgz", - "integrity": "sha512-uG8B0WSepMRsBNVXAQcHf9+Ko/Tr+XqmK7Ptel9HVmnykupXdS4J7ovSQUIi0tQGIndhbqWLaIL/qO/cWhXKyQ==", + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.1.tgz", + "integrity": "sha512-itPwCw5C+Jh/c624vcDd9kRCCZVpzpQn8dtwoYIt2TJF3S9xJLiRohnnNrKwREvcZYx0n8sCSbvGH349XkcQeg==", "cpu": [ "x64" ], @@ -4601,9 +4615,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.0.tgz", - "integrity": "sha512-2ezuhdiZw8vuHf1HKSf4TIk80naTbP9At7sOqZmdVwvvMyuoDiZB49YZKLsLOfKIr77+I40dWpHVeY5JHpIEIg==", + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.1.tgz", + "integrity": "sha512-LojC28v3+IhIbfQ+Vu4Ut5n3wKcgTu6POKIHN9Wpt0HnfgUGlBuyDDQR4jWZUZFyYLiz4RBBBmfU6sNfn6RhLw==", "cpu": [ "arm" ], @@ -4617,9 +4631,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.0.tgz", - "integrity": "sha512-uTtyYAP5veqi2z9b6Gr0NUoNv9F/rOzI8tOD5jKcCvRUn7T60Bb+42NDBCWNhMjkQzI0qqwXkQGo1SY41G52nw==", + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.1.tgz", + "integrity": "sha512-cX8WdlF6Cnvw/DO9/X7XLH2J6CkBnz7Twjpk56cshk9sjYVcuh4sXQBy5bmTwzBjNVZze2yaV1vtcJS04LbN8w==", "cpu": [ "arm64" ], @@ -4633,9 +4647,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.0.tgz", - "integrity": "sha512-c88wwtfs8tTffPaoJ+SQn3y+lKtgTzyjkD8NgsyCtCmtoIC8RDL7PrJU05an/e9VuAke6eJqGkoMhJK1RY6z4w==", + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.1.tgz", + "integrity": "sha512-4H/sQCy1mnnGkUt/xszaLlYJVTz3W9ep52xEefGtd6yXDQbz/5fZE5dFLUgsPdbUOQANcVUa5iO6g3nyy5BJiw==", "cpu": [ "ia32" ], @@ -4649,9 +4663,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.0.tgz", - "integrity": "sha512-lR2rr/128/6svngnVta6JN4gxSXle/yZEZL3o4XZ6esOqhyR4wsKyfu6qXAL04S4S5CgGfG+GYZnjFd4YiG3Aw==", + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.1.tgz", + "integrity": "sha512-c0jgtB+sRHCciVXlyjDcWb2FUuzlGVRwGXgI+3WqKOIuoo8AmZAddzeOHeYLtD+dmtHw3B4Xo9wAUdjlfW5yYA==", "cpu": [ "loong64" ], @@ -4665,9 +4679,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.0.tgz", - "integrity": "sha512-9Sycc+1uUsDnJCelDf6ZNqgZQoK1mJvFtqf2MUz4ujTxGhvCWw+4chYfDLPepMEvVL9PDwn6HrXad5yOrNzIsQ==", + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.1.tgz", + "integrity": "sha512-TgFyCfIxSujyuqdZKDZ3yTwWiGv+KnlOeXXitCQ+trDODJ+ZtGOzLkSWngynP0HZnTsDyBbPy7GWVXWaEl6lhA==", "cpu": [ "mips64el" ], @@ -4681,9 +4695,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.0.tgz", - "integrity": "sha512-CoWSaaAXOZd+CjbUTdXIJE/t7Oz+4g90A3VBCHLbfuc5yUQU/nFDLOzQsN0cdxgXd97lYW/psIIBdjzQIwTBGw==", + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.1.tgz", + "integrity": "sha512-b+yuD1IUeL+Y93PmFZDZFIElwbmFfIKLKlYI8M6tRyzE6u7oEP7onGk0vZRh8wfVGC2dZoy0EqX1V8qok4qHaw==", "cpu": [ "ppc64" ], @@ -4697,9 +4711,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.0.tgz", - "integrity": "sha512-mlb1hg/eYRJUpv8h/x+4ShgoNLL8wgZ64SUr26KwglTYnwAWjkhR2GpoKftDbPOCnodA9t4Y/b68H4J9XmmPzA==", + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.1.tgz", + "integrity": "sha512-wpDlpE0oRKZwX+GfomcALcouqjjV8MIX8DyTrxfyCfXxoKQSDm45CZr9fanJ4F6ckD4yDEPT98SrjvLwIqUCgg==", "cpu": [ "riscv64" ], @@ -4713,9 +4727,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.0.tgz", - "integrity": "sha512-fgf9ubb53xSnOBqyvWEY6ukBNRl1mVX1srPNu06B6mNsNK20JfH6xV6jECzrQ69/VMiTLvHMicQR/PgTOgqJUQ==", + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.1.tgz", + "integrity": "sha512-5BepC2Au80EohQ2dBpyTquqGCES7++p7G+7lXe1bAIvMdXm4YYcEfZtQrP4gaoZ96Wv1Ute61CEHFU7h4FMueQ==", "cpu": [ "s390x" ], @@ -4744,9 +4758,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.0.tgz", - "integrity": "sha512-lCT675rTN1v8Fo+RGrE5KjSnfY0x9Og4RN7t7lVrN3vMSjy34/+3na0q7RIfWDAj0e0rCh0OL+P88lu3Rt21MQ==", + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.1.tgz", + "integrity": "sha512-4fL68JdrLV2nVW2AaWZBv3XEm3Ae3NZn/7qy2KGAt3dexAgSVT+Hc97JKSZnqezgMlv9x6KV0ZkZY7UO5cNLCg==", "cpu": [ "x64" ], @@ -4760,9 +4774,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.0.tgz", - "integrity": "sha512-HKoUGXz/TOVXKQ+67NhxyHv+aDSZf44QpWLa3I1lLvAwGq8x1k0T+e2HHSRvxWhfJrFxaaqre1+YyzQ99KixoA==", + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.1.tgz", + "integrity": "sha512-GhRuXlvRE+twf2ES+8REbeCb/zeikNqwD3+6S5y5/x+DYbAQUNl0HNBs4RQJqrechS4v4MruEr8ZtAin/hK5iw==", "cpu": [ "x64" ], @@ -4776,9 +4790,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.0.tgz", - "integrity": "sha512-GDwAqgHQm1mVoPppGsoq4WJwT3vhnz/2N62CzhvApFD1eJyTroob30FPpOZabN+FgCjhG+AgcZyOPIkR8dfD7g==", + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.1.tgz", + "integrity": "sha512-ZnWEyCM0G1Ex6JtsygvC3KUUrlDXqOihw8RicRuQAzw+c4f1D66YlPNNV3rkjVW90zXVsHwZYWbJh3v+oQFM9Q==", "cpu": [ "x64" ], @@ -4792,9 +4806,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.0.tgz", - "integrity": "sha512-0vYsP8aC4TvMlOQYozoksiaxjlvUcQrac+muDqj1Fxy6jh9l9CZJzj7zmh8JGfiV49cYLTorFLxg7593pGldwQ==", + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.1.tgz", + "integrity": "sha512-QZ6gXue0vVQY2Oon9WyLFCdSuYbXSoxaZrPuJ4c20j6ICedfsDilNPYfHLlMH7vGfU5DQR0czHLmJvH4Nzis/A==", "cpu": [ "arm64" ], @@ -4808,9 +4822,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.0.tgz", - "integrity": "sha512-p98u4rIgfh4gdpV00IqknBD5pC84LCub+4a3MO+zjqvU5MVXOc3hqR2UgT2jI2nh3h8s9EQxmOsVI3tyzv1iFg==", + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.1.tgz", + "integrity": "sha512-HzcJa1NcSWTAU0MJIxOho8JftNp9YALui3o+Ny7hCh0v5f90nprly1U3Sj1Ldj/CvKKdvvFsCRvDkpsEMp4DNw==", "cpu": [ "ia32" ], @@ -4824,9 +4838,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.0.tgz", - "integrity": "sha512-NgJnesu1RtWihtTtXGFMU5YSE6JyyHPMxCwBZK7a6/8d31GuSo9l0Ss7w1Jw5QnKUawG6UEehs883kcXf5fYwg==", + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.1.tgz", + "integrity": "sha512-0MBh53o6XtI6ctDnRMeQ+xoCN8kD2qI1rY1KgF/xdWQwoFeKou7puvDfV8/Wv4Ctx2rRpET/gGdz3YlNtNACSA==", "cpu": [ "x64" ], @@ -4843,6 +4857,7 @@ "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, "dependencies": { "eslint-visitor-keys": "^3.3.0" }, @@ -4857,6 +4872,7 @@ "version": "4.10.0", "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==", + "dev": true, "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } @@ -4865,6 +4881,7 @@ "version": "2.1.4", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", @@ -4887,6 +4904,7 @@ "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -4901,12 +4919,14 @@ "node_modules/@eslint/eslintrc/node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true }, "node_modules/@eslint/eslintrc/node_modules/globals": { "version": "13.24.0", "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, "dependencies": { "type-fest": "^0.20.2" }, @@ -4921,6 +4941,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, "dependencies": { "argparse": "^2.0.1" }, @@ -4931,12 +4952,14 @@ "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true }, "node_modules/@eslint/eslintrc/node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -4948,6 +4971,7 @@ "version": "0.20.2", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, "engines": { "node": ">=10" }, @@ -4959,6 +4983,7 @@ "version": "8.56.0", "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.56.0.tgz", "integrity": "sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A==", + "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } @@ -4979,6 +5004,28 @@ "npm": ">=6.14.13" } }, + "node_modules/@fanoutio/grip": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@fanoutio/grip/-/grip-3.3.1.tgz", + "integrity": "sha512-+zw9p2nIEg4Va0eS8bwKB0axQuxnJnn1LmF0Gs1x3c3YgBlgUjP1krIKude2otKocw10TirnUt2gmvpqz31BqQ==", + "dependencies": { + "agentkeepalive": "^4.1.3", + "debug": "^4.3.4", + "isomorphic-fetch": "^3.0.0", + "jsonwebtoken": "^9.0.2", + "jspack": "0.0.4" + } + }, + "node_modules/@fanoutio/serve-grip": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@fanoutio/serve-grip/-/serve-grip-1.3.1.tgz", + "integrity": "sha512-k1Y3TK+FauznKR+gQCuZWpgUGsHiPPOFCb7P0v8Hxz9vpzvF1mTKWYrld/3qck6k93TRaXe1/0UjjU9Walheiw==", + "dependencies": { + "@fanoutio/grip": "^3.3.1", + "callable-instance": "^2.0.0", + "debug": "^4.3.4" + } + }, "node_modules/@fastify/busboy": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-1.2.1.tgz", @@ -6623,6 +6670,7 @@ "version": "0.11.14", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", + "dev": true, "dependencies": { "@humanwhocodes/object-schema": "^2.0.2", "debug": "^4.3.1", @@ -6636,6 +6684,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, "engines": { "node": ">=12.22" }, @@ -6647,7 +6696,8 @@ "node_modules/@humanwhocodes/object-schema": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz", - "integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==" + "integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==", + "dev": true }, "node_modules/@ionic/angular": { "version": "7.7.4", @@ -7713,14 +7763,14 @@ } }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", - "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", "dev": true, "dependencies": { - "@jridgewell/set-array": "^1.0.1", + "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" + "@jridgewell/trace-mapping": "^0.3.24" }, "engines": { "node": ">=6.0.0" @@ -7736,9 +7786,9 @@ } }, "node_modules/@jridgewell/set-array": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", - "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", "dev": true, "engines": { "node": ">=6.0.0" @@ -7761,9 +7811,9 @@ "dev": true }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.22", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.22.tgz", - "integrity": "sha512-Wf963MzWtA2sjrNt+g18IAln9lKnlRp+K2eH4jjIoF1wYeq3aMREpG09xhlhdzS0EjwU7qmUJYangWa+151vZw==", + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", "dev": true, "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -7797,9 +7847,9 @@ "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" }, "node_modules/@leichtgewicht/ip-codec": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz", - "integrity": "sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", + "integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==", "dev": true }, "node_modules/@ljharb/through": { @@ -7818,6 +7868,8 @@ "version": "1.0.11", "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==", + "optional": true, + "peer": true, "dependencies": { "detect-libc": "^2.0.0", "https-proxy-agent": "^5.0.0", @@ -7836,12 +7888,16 @@ "node_modules/@mapbox/node-pre-gyp/node_modules/abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "optional": true, + "peer": true }, "node_modules/@mapbox/node-pre-gyp/node_modules/agent-base": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "optional": true, + "peer": true, "dependencies": { "debug": "4" }, @@ -7853,6 +7909,8 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", + "optional": true, + "peer": true, "dependencies": { "delegates": "^1.0.0", "readable-stream": "^3.6.0" @@ -7865,6 +7923,8 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", + "optional": true, + "peer": true, "dependencies": { "aproba": "^1.0.3 || ^2.0.0", "color-support": "^1.1.2", @@ -7884,6 +7944,8 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "optional": true, + "peer": true, "dependencies": { "agent-base": "6", "debug": "4" @@ -7896,6 +7958,8 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "optional": true, + "peer": true, "dependencies": { "semver": "^6.0.0" }, @@ -7910,6 +7974,8 @@ "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "optional": true, + "peer": true, "bin": { "semver": "bin/semver.js" } @@ -7918,6 +7984,8 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "optional": true, + "peer": true, "dependencies": { "abbrev": "1" }, @@ -7932,6 +8000,8 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", + "optional": true, + "peer": true, "dependencies": { "are-we-there-yet": "^2.0.0", "console-control-strings": "^1.1.0", @@ -7943,6 +8013,8 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "optional": true, + "peer": true, "dependencies": { "glob": "^7.1.3" }, @@ -7954,9 +8026,9 @@ } }, "node_modules/@ngtools/webpack": { - "version": "17.2.0", - "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-17.2.0.tgz", - "integrity": "sha512-3VilWAMylVpOqffhnLdc/UeElUWhBbG5j2XzxYWfQXb8OcVYoKNYPmJLc1vemoaYkkbaUX3zc5AEAN93Hk/q/g==", + "version": "17.3.2", + "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-17.3.2.tgz", + "integrity": "sha512-E8zejFF4aJ8l2XcF+GgnE/1IqsZepnPT1xzulLB4LXtjVuXLFLoF9xkHQwxs7cJWWZsxd/SlNsCIcn/ezrYBcQ==", "dev": true, "engines": { "node": "^18.13.0 || >=20.9.0", @@ -7965,7 +8037,7 @@ }, "peerDependencies": { "@angular/compiler-cli": "^17.0.0", - "typescript": ">=5.2 <5.4", + "typescript": ">=5.2 <5.5", "webpack": "^5.54.0" } }, @@ -8010,6 +8082,7 @@ "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" @@ -8022,6 +8095,7 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, "engines": { "node": ">= 8" } @@ -8030,6 +8104,7 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" @@ -11925,6 +12000,7 @@ "version": "8.11.3", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "devOptional": true, "bin": { "acorn": "bin/acorn" }, @@ -11964,6 +12040,7 @@ "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "devOptional": true, "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } @@ -12230,7 +12307,9 @@ "node_modules/aproba": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", - "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==" + "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==", + "optional": true, + "peer": true }, "node_modules/arg": { "version": "4.1.3", @@ -12578,9 +12657,9 @@ } }, "node_modules/autoprefixer": { - "version": "10.4.17", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.17.tgz", - "integrity": "sha512-/cpVNRLSfhOtcGflT13P2794gVSgmPgTR+erw5ifnMLZb0UnSlkK4tquLmkd3BhA+nLo5tX8Cu0upUsGKvKbmg==", + "version": "10.4.18", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.18.tgz", + "integrity": "sha512-1DKbDfsr6KUElM6wg+0zRNkB/Q7WcKYAaK+pzXn+Xqmszm/5Xa9coeNdtP88Vi+dPzZnMjhge8GIV49ZQkDa+g==", "dev": true, "funding": [ { @@ -12597,8 +12676,8 @@ } ], "dependencies": { - "browserslist": "^4.22.2", - "caniuse-lite": "^1.0.30001578", + "browserslist": "^4.23.0", + "caniuse-lite": "^1.0.30001591", "fraction.js": "^4.3.7", "normalize-range": "^0.1.2", "picocolors": "^1.0.0", @@ -14008,10 +14087,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/callable-instance": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/callable-instance/-/callable-instance-2.0.0.tgz", + "integrity": "sha512-wOBp/J1CRZLsbFxG1alxefJjoG1BW/nocXkUanAe2+leiD/+cVr00j8twSZoDiRy03o5vibq9pbrZc+EDjjUTw==" + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, "engines": { "node": ">=6" } @@ -14043,9 +14128,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001588", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001588.tgz", - "integrity": "sha512-+hVY9jE44uKLkH0SrUTqxjxqNTOWHsbnQDIKjwkZ3lNTzUUVdBLBGXtj/q5Mp5u98r3droaZAewQuEDzjQdZlQ==", + "version": "1.0.30001603", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001603.tgz", + "integrity": "sha512-iL2iSS0eDILMb9n5yKQoTBim9jMZ0Yrk8g0N9K7UzYyWnfIKzXBZD5ngpM37ZcL/cv0Mli8XtVMRYMQAfFpi5Q==", "dev": true, "funding": [ { @@ -14067,6 +14152,8 @@ "resolved": "https://registry.npmjs.org/canvas/-/canvas-2.11.2.tgz", "integrity": "sha512-ItanGBMrmRV7Py2Z+Xhs7cT+FNt5K0vPL4p9EZ/UX/Mu7hFbkxSjKF2KVtPwX7UYWp7dRKnrTvReflgrItJbdw==", "hasInstallScript": true, + "optional": true, + "peer": true, "dependencies": { "@mapbox/node-pre-gyp": "^1.0.0", "nan": "^2.17.0", @@ -14080,6 +14167,8 @@ "version": "4.2.1", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz", "integrity": "sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==", + "optional": true, + "peer": true, "dependencies": { "mimic-response": "^2.0.0" }, @@ -14091,6 +14180,8 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz", "integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==", + "optional": true, + "peer": true, "engines": { "node": ">=8" }, @@ -14102,6 +14193,8 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-3.1.1.tgz", "integrity": "sha512-CQ5LTKGfCpvE1K0n2us+kuMPbk/q0EKl82s4aheV9oXjFEz6W/Y7oQFVJuU6QG77hRT4Ghb5RURteF5vnWjupA==", + "optional": true, + "peer": true, "dependencies": { "decompress-response": "^4.2.0", "once": "^1.3.1", @@ -14238,6 +14331,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "devOptional": true, "engines": { "node": ">=10" } @@ -14638,6 +14732,8 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "optional": true, + "peer": true, "bin": { "color-support": "bin.js" } @@ -14879,7 +14975,9 @@ "node_modules/console-control-strings": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==" + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "optional": true, + "peer": true }, "node_modules/constantinople": { "version": "4.0.1", @@ -15186,9 +15284,9 @@ "dev": true }, "node_modules/critters": { - "version": "0.0.20", - "resolved": "https://registry.npmjs.org/critters/-/critters-0.0.20.tgz", - "integrity": "sha512-CImNRorKOl5d8TWcnAz5n5izQ6HFsvz29k327/ELy6UFcmbiZNOsinaKvzv16WZR0P6etfSWYzE47C4/56B3Uw==", + "version": "0.0.22", + "resolved": "https://registry.npmjs.org/critters/-/critters-0.0.22.tgz", + "integrity": "sha512-NU7DEcQZM2Dy8XTKFHxtdnIM/drE312j2T4PCVaSUcS0oBeyT/NImpRw/Ap0zOr/1SE7SgPK9tGPg1WK/sVakw==", "dev": true, "dependencies": { "chalk": "^4.1.0", @@ -15197,7 +15295,7 @@ "domhandler": "^5.0.2", "htmlparser2": "^8.0.2", "postcss": "^8.4.23", - "pretty-bytes": "^5.3.0" + "postcss-media-query-parser": "^0.2.3" } }, "node_modules/critters/node_modules/ansi-styles": { @@ -15282,6 +15380,7 @@ "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -15754,7 +15853,9 @@ "node_modules/delegates": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==" + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "optional": true, + "peer": true }, "node_modules/depd": { "version": "2.0.0", @@ -15905,6 +16006,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, "dependencies": { "esutils": "^2.0.2" }, @@ -16574,9 +16676,9 @@ } }, "node_modules/esbuild-wasm": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/esbuild-wasm/-/esbuild-wasm-0.20.0.tgz", - "integrity": "sha512-Lc9KeQCg1Zf8kCtfDXgy29rx0x8dOuhDWbkP76Wc64q7ctOOc1Zv1C39AxiE+y4N6ONyXtJk4HKpM7jlU7/jSA==", + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/esbuild-wasm/-/esbuild-wasm-0.20.1.tgz", + "integrity": "sha512-6v/WJubRsjxBbQdz6izgvx7LsVFvVaGmSdwrFHmEzoVgfXL89hkKPoQHsnVI2ngOkcBUQT9kmAM1hVL1k/Av4A==", "dev": true, "bin": { "esbuild": "bin/esbuild" @@ -17009,6 +17111,7 @@ "version": "8.46.0", "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.46.0.tgz", "integrity": "sha512-cIO74PvbW0qU8e0mIvk5IV3ToWdCq5FYG6gWPHHkx6gNdjlbAYvtfHmlCMXxjcoVaIdwy/IAt3+mDkZkfvb2Dg==", + "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -17356,26 +17459,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/eslint-plugin-prettier": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-3.4.1.tgz", - "integrity": "sha512-htg25EUYUeIhKHXjOinK4BgCcDwtLHjqaxCDsMy5nbnUMkKFvIhMVCp+5GFUXQ4Nr8lBsPqtGAqBenbpFqAA2g==", - "dependencies": { - "prettier-linter-helpers": "^1.0.0" - }, - "engines": { - "node": ">=6.0.0" - }, - "peerDependencies": { - "eslint": ">=5.0.0", - "prettier": ">=1.13.0" - }, - "peerDependenciesMeta": { - "eslint-config-prettier": { - "optional": true - } - } - }, "node_modules/eslint-plugin-promise": { "version": "6.1.1", "resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-6.1.1.tgz", @@ -17408,6 +17491,7 @@ "version": "3.4.3", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "devOptional": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, @@ -17419,6 +17503,7 @@ "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -17434,6 +17519,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -17447,12 +17533,14 @@ "node_modules/eslint/node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true }, "node_modules/eslint/node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -17468,6 +17556,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, "dependencies": { "color-name": "~1.1.4" }, @@ -17478,12 +17567,14 @@ "node_modules/eslint/node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true }, "node_modules/eslint/node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, "engines": { "node": ">=10" }, @@ -17495,6 +17586,7 @@ "version": "7.2.2", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" @@ -17510,6 +17602,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" @@ -17525,6 +17618,7 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, "dependencies": { "is-glob": "^4.0.3" }, @@ -17536,6 +17630,7 @@ "version": "13.24.0", "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, "dependencies": { "type-fest": "^0.20.2" }, @@ -17550,6 +17645,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, "engines": { "node": ">=8" } @@ -17558,6 +17654,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, "dependencies": { "argparse": "^2.0.1" }, @@ -17568,12 +17665,14 @@ "node_modules/eslint/node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true }, "node_modules/eslint/node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, "dependencies": { "p-locate": "^5.0.0" }, @@ -17588,6 +17687,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -17599,6 +17699,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, "dependencies": { "p-limit": "^3.0.2" }, @@ -17613,6 +17714,7 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -17624,6 +17726,7 @@ "version": "0.20.2", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, "engines": { "node": ">=10" }, @@ -17654,6 +17757,7 @@ "version": "9.6.1", "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "devOptional": true, "dependencies": { "acorn": "^8.9.0", "acorn-jsx": "^5.3.2", @@ -17682,6 +17786,7 @@ "version": "1.5.0", "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "dev": true, "dependencies": { "estraverse": "^5.1.0" }, @@ -17693,6 +17798,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, "dependencies": { "estraverse": "^5.2.0" }, @@ -17924,16 +18030,16 @@ "dev": true }, "node_modules/express": { - "version": "4.18.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", - "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", + "version": "4.19.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", + "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.1", + "body-parser": "1.20.2", "content-disposition": "0.5.4", "content-type": "~1.0.4", - "cookie": "0.5.0", + "cookie": "0.6.0", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", @@ -17964,44 +18070,10 @@ "node": ">= 0.10.0" } }, - "node_modules/express-grip": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/express-grip/-/express-grip-1.2.1.tgz", - "integrity": "sha512-PZiuc27+pgdvjTsz5mk31nK1ECgURtjj1XdD0c6jxEB4fEyeciJ9Lth27bOq0x798/oLk5couxAVKTFiEhRHLA==", - "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", - "dependencies": { - "grip": "^1.3.0", - "jspack": "0.0.4", - "pubcontrol": "^1.2.0" - } - }, - "node_modules/express/node_modules/body-parser": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", - "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", - "dependencies": { - "bytes": "3.1.2", - "content-type": "~1.0.4", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "on-finished": "2.4.1", - "qs": "6.11.0", - "raw-body": "2.5.1", - "type-is": "~1.6.18", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, "node_modules/express/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.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", "engines": { "node": ">= 0.6" } @@ -18019,20 +18091,6 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, - "node_modules/express/node_modules/raw-body": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", - "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", - "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/ext": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz", @@ -18162,11 +18220,6 @@ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, - "node_modules/fast-diff": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", - "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==" - }, "node_modules/fast-fifo": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", @@ -18244,6 +18297,7 @@ "version": "1.16.0", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.16.0.tgz", "integrity": "sha512-ifCoaXsDrsdkWTtiNJX5uzHDsrck5TzfKKDcuFFTIrrc/BS076qgEIfoIy1VeZqViznfKiysPYTh/QeHtnIsYA==", + "dev": true, "dependencies": { "reusify": "^1.0.4" } @@ -18307,6 +18361,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, "dependencies": { "flat-cache": "^3.0.4" }, @@ -18549,6 +18604,7 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.3", @@ -18562,6 +18618,7 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, "dependencies": { "glob": "^7.1.3" }, @@ -18575,7 +18632,8 @@ "node_modules/flatted": { "version": "3.2.9", "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz", - "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==" + "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==", + "dev": true }, "node_modules/follow-redirects": { "version": "1.15.6", @@ -19442,21 +19500,8 @@ "node_modules/graphemer": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==" - }, - "node_modules/grip": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/grip/-/grip-1.6.0.tgz", - "integrity": "sha512-cFmQ4D/QVS4Bp7yXCHR79jmEkEBqJ+QRoMXUlFM7x3So7OtaRJUz1SemhtQq73a8Gq0rh45g2LbPfiyhsi6Ukg==", - "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", - "dependencies": { - "jspack": "0.0.4", - "jwt-simple": "^0.5.6", - "pubcontrol": "1.x" - }, - "engines": { - "node": ">=1.5.0" - } + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true }, "node_modules/growly": { "version": "1.3.0", @@ -19672,7 +19717,9 @@ "node_modules/has-unicode": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", - "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==" + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", + "optional": true, + "peer": true }, "node_modules/has-value": { "version": "1.0.0", @@ -19870,9 +19917,9 @@ } }, "node_modules/html-entities": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.4.0.tgz", - "integrity": "sha512-igBTJcNNNhvZFRtm8uA6xMY6xYleeDwn3PeBCkDz7tHttv4F2hsDI2aPgNERWzvRcNYHNT3ymRaQzllmXj4YsQ==", + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.5.2.tgz", + "integrity": "sha512-K//PSRMQk4FZ78Kyau+mZurHn3FH0Vwr+H36eE0rPbeYkRRi9YxceYPhuN60UwWorxyKHhqoAJl2OFKa4BVtaA==", "dev": true, "funding": [ { @@ -20006,9 +20053,9 @@ } }, "node_modules/https-proxy-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.2.tgz", - "integrity": "sha512-NmLNjm6ucYwtcUmL7JQC1ZQ57LmHP4lT15FQ8D61nak1rO6DH+fz5qNK2Ap5UN4ZapYICE3/0KodcLYSPsPbaA==", + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz", + "integrity": "sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==", "dependencies": { "agent-base": "^7.0.2", "debug": "4" @@ -20162,6 +20209,7 @@ "version": "5.3.0", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.0.tgz", "integrity": "sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg==", + "dev": true, "engines": { "node": ">= 4" } @@ -20225,6 +20273,7 @@ "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" @@ -20240,6 +20289,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, "engines": { "node": ">=4" } @@ -20729,6 +20779,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, "engines": { "node": ">=0.10.0" } @@ -20765,6 +20816,7 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, "dependencies": { "is-extglob": "^2.1.1" }, @@ -20865,6 +20917,7 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, "engines": { "node": ">=8" } @@ -21092,6 +21145,15 @@ "node": ">=0.10.0" } }, + "node_modules/isomorphic-fetch": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-3.0.0.tgz", + "integrity": "sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA==", + "dependencies": { + "node-fetch": "^2.6.1", + "whatwg-fetch": "^3.4.1" + } + }, "node_modules/isstream": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", @@ -23865,7 +23927,8 @@ "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==" + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true }, "node_modules/json-parse-even-better-errors": { "version": "3.0.1", @@ -23907,7 +23970,8 @@ "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==" + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true }, "node_modules/json-stringify-safe": { "version": "5.0.1", @@ -24094,14 +24158,6 @@ "safe-buffer": "^5.0.1" } }, - "node_modules/jwt-simple": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/jwt-simple/-/jwt-simple-0.5.6.tgz", - "integrity": "sha512-40aUybvhH9t2h71ncA1/1SbtTNCVZHgsTsTgqPUxGWDmUDrXyDf2wMNQKEbdBjbf4AI+fQhbECNTV6lWxQKUzg==", - "engines": { - "node": ">= 0.4.0" - } - }, "node_modules/karma-source-map-support": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/karma-source-map-support/-/karma-source-map-support-1.4.0.tgz", @@ -24115,6 +24171,7 @@ "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, "dependencies": { "json-buffer": "3.0.1" } @@ -24299,6 +24356,7 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" @@ -24794,7 +24852,8 @@ "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true }, "node_modules/lodash.once": { "version": "4.1.1", @@ -25621,9 +25680,9 @@ } }, "node_modules/mini-css-extract-plugin": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.8.0.tgz", - "integrity": "sha512-CxmUYPFcTgET1zImteG/LZOy/4T5rTojesQXkSNBiquhydn78tfbCE9sjIjnJ/UcjNjOC1bphTCCW5rrS7cXAg==", + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.8.1.tgz", + "integrity": "sha512-/1HDlyFRxWIZPI1ZpgqlZ8jMw/1Dp/dl3P0L1jtZ+zVcHqwPhGwaJwKL00WVgfnBy6PWCde9W65or7IIETImuA==", "dev": true, "dependencies": { "schema-utils": "^4.0.0", @@ -25857,6 +25916,7 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "devOptional": true, "dependencies": { "minipass": "^3.0.0", "yallist": "^4.0.0" @@ -25869,6 +25929,7 @@ "version": "3.3.6", "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "devOptional": true, "dependencies": { "yallist": "^4.0.0" }, @@ -25879,7 +25940,8 @@ "node_modules/minizlib/node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "devOptional": true }, "node_modules/mitt": { "version": "3.0.1", @@ -26096,7 +26158,8 @@ "node_modules/nan": { "version": "2.18.0", "resolved": "https://registry.npmjs.org/nan/-/nan-2.18.0.tgz", - "integrity": "sha512-W7tfG7vMOGtD30sHoZSSc/JVYiyDPEyQVso/Zz+/uQd0B0L46gtC+pHha5FFMRpil6fm/AoEcRWyOVi4+E/f8w==" + "integrity": "sha512-W7tfG7vMOGtD30sHoZSSc/JVYiyDPEyQVso/Zz+/uQd0B0L46gtC+pHha5FFMRpil6fm/AoEcRWyOVi4+E/f8w==", + "optional": true }, "node_modules/nanoid": { "version": "3.3.7", @@ -27283,6 +27346,7 @@ "version": "0.9.3", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", + "dev": true, "dependencies": { "@aashutoshrathi/word-wrap": "^1.2.3", "deep-is": "^0.1.3", @@ -27725,6 +27789,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, "dependencies": { "callsites": "^3.0.0" }, @@ -27866,6 +27931,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, "engines": { "node": ">=8" } @@ -27882,6 +27948,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, "engines": { "node": ">=8" } @@ -27930,15 +27997,6 @@ "node": ">=8" } }, - "node_modules/path2d-polyfill": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path2d-polyfill/-/path2d-polyfill-2.0.1.tgz", - "integrity": "sha512-ad/3bsalbbWhmBo0D6FZ4RNMwsLsPpL6gnvhuSaU5Vm7b06Kr5ubSltQQ0T7YKsiJQO+g22zJ4dJKNTXIyOXtA==", - "optional": true, - "engines": { - "node": ">=8" - } - }, "node_modules/pathval": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", @@ -27948,18 +28006,6 @@ "node": "*" } }, - "node_modules/pdfjs-dist": { - "version": "3.11.174", - "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-3.11.174.tgz", - "integrity": "sha512-TdTZPf1trZ8/UFu5Cx/GXB7GZM30LT+wWUNfsi6Bq8ePLnb+woNKtDymI2mxZYBpMbonNFqKmiz684DIfnd8dA==", - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "canvas": "^2.11.2", - "path2d-polyfill": "^2.0.1" - } - }, "node_modules/pdfmake": { "version": "0.2.9", "resolved": "https://registry.npmjs.org/pdfmake/-/pdfmake-0.2.9.tgz", @@ -28157,9 +28203,9 @@ } }, "node_modules/piscina": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/piscina/-/piscina-4.3.1.tgz", - "integrity": "sha512-MBj0QYm3hJQ/C/wIXTN1OCYC8uQ4BBJ4LVele2P4ZwVQAH04vkk8E1SpDbuemLAL1dZorbuOob9rYqJeWCcCRg==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/piscina/-/piscina-4.4.0.tgz", + "integrity": "sha512-+AQduEJefrOApE4bV7KRmp3N2JnnyErlVqq4P/jmko4FPz9Z877BCccl/iB3FdrWSUkvbGV9Kan/KllJgat3Vg==", "dev": true, "optionalDependencies": { "nice-napi": "^1.0.2" @@ -28330,9 +28376,9 @@ } }, "node_modules/postcss-loader": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-8.1.0.tgz", - "integrity": "sha512-AbperNcX3rlob7Ay7A/HQcrofug1caABBkopoFeOQMspZBqcqj6giYn1Bwey/0uiOPAcR+NQD0I2HC7rXzk91w==", + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-8.1.1.tgz", + "integrity": "sha512-0IeqyAsG6tYiDRCYKQJLAmgQr47DX6N7sFSWvQxt6AcupX8DIdmykuk/o/tx0Lze3ErGHJEp5OSRxrelC6+NdQ==", "dev": true, "dependencies": { "cosmiconfig": "^9.0.0", @@ -28404,6 +28450,12 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/postcss-media-query-parser": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/postcss-media-query-parser/-/postcss-media-query-parser-0.2.3.tgz", + "integrity": "sha512-3sOlxmbKcSHMjlUXQZKQ06jOswE7oVkXPxmZdoB1r5l0q6gTFTQSHxNxOrCccElbW7dxNytifNEo8qidX2Vsig==", + "dev": true + }, "node_modules/postcss-modules-extract-imports": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz", @@ -28562,6 +28614,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, "engines": { "node": ">= 0.8.0" } @@ -28587,6 +28640,7 @@ "version": "3.2.4", "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.4.tgz", "integrity": "sha512-FWu1oLHKCrtpO1ypU6J0SbK2d9Ckwysq6bHj/uaCP26DxrPpppCLQRGVuqAxSTvhF00AcvDRyYrLNW7ocBhFFQ==", + "dev": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -28597,17 +28651,6 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, - "node_modules/prettier-linter-helpers": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", - "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", - "dependencies": { - "fast-diff": "^1.1.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/pretty-bytes": { "version": "5.6.0", "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", @@ -28849,26 +28892,6 @@ "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==" }, - "node_modules/pubcontrol": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/pubcontrol/-/pubcontrol-1.3.0.tgz", - "integrity": "sha512-cDYZgthXSC2MZlB0gG1xMEln+z9MtqiGJh8+eSxsTM44v8yujRD7dn9nfY31gkM0maFi9pura+QeFKhyls6uXA==", - "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", - "dependencies": { - "agentkeepalive": "2.x", - "eslint-plugin-prettier": "^3.0.0", - "jwt-simple": "0.x", - "node-fetch": "^2.1.2" - } - }, - "node_modules/pubcontrol/node_modules/agentkeepalive": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-2.2.0.tgz", - "integrity": "sha512-TnB6ziK363p7lR8QpeLC8aMr8EGYBKZTpgzQLfqTs3bR0Oo5VbKdwKf8h0dSzsYrB7lSCgfJnMZKqShvlq5Oyg==", - "engines": { - "node": ">= 0.10.0" - } - }, "node_modules/pug": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/pug/-/pug-3.0.2.tgz", @@ -29073,6 +29096,7 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, "funding": [ { "type": "github", @@ -29955,6 +29979,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, "engines": { "iojs": ">=1.0.0", "node": ">=0.10.0" @@ -30093,6 +30118,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, "funding": [ { "type": "github", @@ -30505,9 +30531,9 @@ } }, "node_modules/sass": { - "version": "1.70.0", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.70.0.tgz", - "integrity": "sha512-uUxNQ3zAHeAx5nRFskBnrWzDUJrrvpCPD5FNAoRvTi0WwremlheES3tg+56PaVtCs5QDRX5CBLxxKMDJMEa1WQ==", + "version": "1.71.1", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.71.1.tgz", + "integrity": "sha512-wovtnV2PxzteLlfNzbgm1tFXPLoZILYAMJtvoXXkD7/+1uP41eKkIt1ypWq5/q2uT94qHjXehEYfmjKOvjL9sg==", "dev": true, "dependencies": { "chokidar": ">=3.0.0 <4.0.0", @@ -30522,9 +30548,9 @@ } }, "node_modules/sass-loader": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-14.1.0.tgz", - "integrity": "sha512-LS2mLeFWA+orYxHNu+O18Xe4jR0kyamNOOUsE3NyBP4DvIL+8stHpNX0arYTItdPe80kluIiJ7Wfe/9iHSRO0Q==", + "version": "14.1.1", + "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-14.1.1.tgz", + "integrity": "sha512-QX8AasDg75monlybel38BZ49JP5Z+uSKfKwF2rO7S74BywaRmGQMUBw9dtkS+ekyM/QnP+NOrRYq8ABMZ9G8jw==", "dev": true, "dependencies": { "neo-async": "^2.6.2" @@ -31120,6 +31146,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, "dependencies": { "shebang-regex": "^3.0.0" }, @@ -31131,6 +31158,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, "engines": { "node": ">=8" } @@ -32252,6 +32280,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "devOptional": true, "engines": { "node": ">=8" }, @@ -32447,6 +32476,7 @@ "version": "6.2.0", "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.0.tgz", "integrity": "sha512-/Wo7DcT0u5HUV486xg675HtjNd3BXZ6xDbzsCUZPt5iw8bTQ63bP0Raut3mvro9u+CUyq7YQd8Cx55fsZXxqLQ==", + "devOptional": true, "dependencies": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", @@ -32498,6 +32528,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "devOptional": true, "dependencies": { "minipass": "^3.0.0" }, @@ -32509,6 +32540,7 @@ "version": "3.3.6", "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "devOptional": true, "dependencies": { "yallist": "^4.0.0" }, @@ -32520,6 +32552,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "devOptional": true, "engines": { "node": ">=8" } @@ -32528,6 +32561,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "devOptional": true, "bin": { "mkdirp": "bin/cmd.js" }, @@ -32538,7 +32572,8 @@ "node_modules/tar/node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "devOptional": true }, "node_modules/teeny-request": { "version": "8.0.3", @@ -32647,9 +32682,9 @@ } }, "node_modules/terser": { - "version": "5.27.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.27.0.tgz", - "integrity": "sha512-bi1HRwVRskAjheeYl291n3JC4GgO/Ty4z1nVs5AAsmonJulGxpSektecnNedrwK9C7vpvVtcX3cw00VSLt7U2A==", + "version": "5.29.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.29.1.tgz", + "integrity": "sha512-lZQ/fyaIGxsbGxApKmoPTODIzELy3++mXhS5hOqaAWZjQtpq/hFHAc+rm29NND1rYRxRWKcjuARNwULNXa5RtQ==", "dev": true, "dependencies": { "@jridgewell/source-map": "^0.3.3", @@ -32813,7 +32848,8 @@ "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==" + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true }, "node_modules/throat": { "version": "4.1.0", @@ -33236,6 +33272,7 @@ "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, "dependencies": { "prelude-ls": "^1.2.1" }, @@ -33503,13 +33540,10 @@ "integrity": "sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==" }, "node_modules/undici": { - "version": "6.6.2", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.6.2.tgz", - "integrity": "sha512-vSqvUE5skSxQJ5sztTZ/CdeJb1Wq0Hf44hlYMciqHghvz+K88U0l7D6u1VsndoFgskDcnU+nG3gYmMzJVzd9Qg==", + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.7.1.tgz", + "integrity": "sha512-+Wtb9bAQw6HYWzCnxrPTMVEV3Q1QjYanI0E4q02ehReMuquQdLTEFEYbfs7hcImVYKcQkWSwT6buEmSVIiDDtQ==", "dev": true, - "dependencies": { - "@fastify/busboy": "^2.0.0" - }, "engines": { "node": ">=18.0" } @@ -33519,15 +33553,6 @@ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" }, - "node_modules/undici/node_modules/@fastify/busboy": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.0.tgz", - "integrity": "sha512-+KpH+QxZU7O4675t3mnkQKcZZg56u+K/Ct2K+N2AZYNVK8kyeo/bI18tI8aPm3tvNNRyTWfj6s5tnGNlcbQRsA==", - "dev": true, - "engines": { - "node": ">=14" - } - }, "node_modules/unicode-canonical-property-names-ecmascript": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", @@ -35352,13 +35377,13 @@ "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==" }, "node_modules/vite": { - "version": "5.0.12", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.0.12.tgz", - "integrity": "sha512-4hsnEkG3q0N4Tzf1+t6NdN9dg/L3BM+q8SWgbSPnJvrgH2kgdyzfVJwbR1ic69/4uMJJ/3dqDZZE5/WwqW8U1w==", + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.1.5.tgz", + "integrity": "sha512-BdN1xh0Of/oQafhU+FvopafUp6WaYenLU/NFoL5WyJL++GxkNfieKzBhM24H3HVsPQrlAqB7iJYTHabzaRed5Q==", "dev": true, "dependencies": { "esbuild": "^0.19.3", - "postcss": "^8.4.32", + "postcss": "^8.4.35", "rollup": "^4.2.0" }, "bin": { @@ -35514,9 +35539,9 @@ } }, "node_modules/webpack": { - "version": "5.90.1", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.90.1.tgz", - "integrity": "sha512-SstPdlAC5IvgFnhiRok8hqJo/+ArAbNv7rhU4fnWGHNVfN59HSQFaxZDSAL3IFG2YmqxuRs+IU33milSxbPlog==", + "version": "5.90.3", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.90.3.tgz", + "integrity": "sha512-h6uDYlWCctQRuXBs1oYpVe6sFcWedl0dpcVaTf/YF67J9bKvwJajFulMVSYKHrksMB3I/pIagRzDxwxkebuzKA==", "dev": true, "dependencies": { "@types/eslint-scope": "^3.7.3", @@ -35615,9 +35640,9 @@ } }, "node_modules/webpack-dev-middleware": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-6.1.1.tgz", - "integrity": "sha512-y51HrHaFeeWir0YO4f0g+9GwZawuigzcAdRNon6jErXy/SqV/+O6eaVAzDqE6t3e3NpGeR5CS+cCDaTC+V3yEQ==", + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-6.1.2.tgz", + "integrity": "sha512-Wu+EHmX326YPYUpQLKmKbTyZZJIB8/n6R09pTmB03kJmnMsVPTo9COzHZFr01txwaCAuZvfBJE4ZCHRcKs5JaQ==", "dev": true, "dependencies": { "colorette": "^2.0.10", @@ -35912,6 +35937,11 @@ "node": ">=0.10.0" } }, + "node_modules/whatwg-fetch": { + "version": "3.6.20", + "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz", + "integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==" + }, "node_modules/whatwg-mimetype": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", @@ -35936,6 +35966,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, "dependencies": { "isexe": "^2.0.0" }, @@ -35988,6 +36019,8 @@ "version": "1.1.5", "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "optional": true, + "peer": true, "dependencies": { "string-width": "^1.0.2 || 2 || 3 || 4" } diff --git a/package.json b/package.json index 7a3b10329..05e9f8f7f 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,8 @@ "@capacitor/ios": "5.5.1", "@capawesome/capacitor-file-picker": "^5.3.0", "@elastic/elasticsearch": "~8.10.0", + "@fanoutio/grip": "^3.3.1", + "@fanoutio/serve-grip": "^1.3.1", "@google-cloud/vision": "^4.0.2", "@ionic/angular": "^7.7.3", "@julianpoy/recipe-clipper": "^3.1.0", @@ -35,22 +37,19 @@ "@trpc/server": "^10.43.6", "axios": "^1.6.8", "body-parser": "~1.20.2", - "canvas": "^2.11.2", "commander": "^11.1.0", "cookie-parser": "^1.4.6", "core-js": "^3.33.2", "cors": "^2.8.5", "dayjs": "^1.11.10", "debug": "~4.3.4", - "express": "~4.18.2", - "express-grip": "^1.2.1", + "express": "~4.19.2", "extract-zip": "^2.0.1", "firebase": "^10.8.1", "firebase-admin": "^11.11.0", "fraction.js": "^4.3.7", "fs-extra": "^11.1.1", "google-auth-library": "^9.4.2", - "grip": "^1.5.0", "he": "^1.2.0", "https-proxy-agent": "^7.0.2", "ical-generator": "^6.0.0", @@ -67,7 +66,6 @@ "node-fetch": "^2.6.7", "openai": "^4.24.1", "p-limit": "^3.1.0", - "pdfjs-dist": "^3.11.174", "pdfmake": "^0.2.8", "pg": "8.11.3", "pg-hstore": "^2.3.4", @@ -95,7 +93,7 @@ }, "devDependencies": { "@angular-devkit/architect": "0.1701.0", - "@angular-devkit/build-angular": "17.2.0", + "@angular-devkit/build-angular": "17.3.2", "@angular-devkit/core": "17.1.0", "@angular-devkit/schematics": "17.1.0", "@angular-eslint/builder": "17.2.1", diff --git a/packages/backend/src/app.ts b/packages/backend/src/app.ts index 7b39028ea..c0f55622a 100644 --- a/packages/backend/src/app.ts +++ b/packages/backend/src/app.ts @@ -31,7 +31,6 @@ import clip from "./routes/clip.js"; import data from "./routes/data.js"; import proxy from "./routes/proxy.js"; -import ws from "./routes/ws.js"; import { ErrorRequestHandler } from "express"; const app = express(); @@ -91,7 +90,6 @@ app.use("/images", images); app.use("/clip", clip); app.use("/proxy", proxy); app.use("/data", data); -app.use("/ws", ws); // catch 404 and forward to error handler app.use(function (req, res, next) { diff --git a/packages/backend/src/models/mealplanitem.js b/packages/backend/src/models/mealplanitem.js index 8ec2f1d4f..952f3019e 100644 --- a/packages/backend/src/models/mealplanitem.js +++ b/packages/backend/src/models/mealplanitem.js @@ -13,6 +13,10 @@ export const MealPlanItemInit = (sequelize, DataTypes) => { allowNull: false, }, scheduled: { + type: DataTypes.DATE, + allowNull: true, + }, + scheduledDate: { type: DataTypes.DATE, allowNull: false, }, diff --git a/packages/backend/src/routes/mealPlans.js b/packages/backend/src/routes/mealPlans.js index a225ecef8..9b79c9bf7 100644 --- a/packages/backend/src/routes/mealPlans.js +++ b/packages/backend/src/routes/mealPlans.js @@ -1,7 +1,6 @@ import * as express from "express"; const router = express.Router(); import * as cors from "cors"; -import ical from "ical-generator"; // DB import { Op } from "sequelize"; @@ -18,7 +17,7 @@ import { // Service import * as MiddlewareService from "../services/middleware.js"; -import * as GripService from "../services/grip.js"; +import { broadcastWSEvent } from "@recipesage/util/server/general"; // Util import { wrapRequestWithErrorHandler } from "../utils/wrapRequestWithErrorHandler.js"; @@ -49,7 +48,7 @@ router.post( }); for (let i = 0; i < (req.body.collaborators || []).length; i++) { - GripService.broadcast(req.body.collaborators[i], "mealPlan:received", { + broadcastWSEvent(req.body.collaborators[i], "mealPlan:received", { mealPlanId: mealPlan.id, from: { id: res.locals.user.id, @@ -165,9 +164,14 @@ router.post( ); } + // REST api does not support new date format + const legacyScheduled = new Date(req.body.scheduled); + const legacyScheduledDate = legacyScheduled.toISOString().split("T")[0]; + await MealPlanItem.create({ title: req.body.title, - scheduled: new Date(req.body.scheduled), + scheduled: legacyScheduled, + scheduledDate: legacyScheduledDate, meal: req.body.meal, recipeId: req.body.recipeId || null, userId: res.locals.session.userId, @@ -186,13 +190,13 @@ router.post( reference, }; - GripService.broadcast( + broadcastWSEvent( mealPlan.userId, "mealPlan:itemsUpdated", broadcastPayload, ); for (let i = 0; i < mealPlan.collaborators.length; i++) { - GripService.broadcast( + broadcastWSEvent( mealPlan.collaborators[i].id, "mealPlan:itemsUpdated", broadcastPayload, @@ -236,7 +240,7 @@ router.delete( if (mealPlan.userId === res.locals.session.userId) { await mealPlan.destroy(); for (let i = 0; i < (mealPlan.collaborators || []).length; i++) { - GripService.broadcast(mealPlan.collaborators[i], "mealPlan:removed", { + broadcastWSEvent(mealPlan.collaborators[i], "mealPlan:removed", { mealPlanId: mealPlan.id, updatedBy: { id: res.locals.user.id, @@ -300,13 +304,13 @@ router.delete( reference, }; - GripService.broadcast( + broadcastWSEvent( mealPlan.userId, "mealPlan:itemsUpdated", deletedItemBroadcast, ); for (let i = 0; i < mealPlan.collaborators.length; i++) { - GripService.broadcast( + broadcastWSEvent( mealPlan.collaborators[i].id, "mealPlan:itemsUpdated", deletedItemBroadcast, @@ -382,13 +386,9 @@ router.put( reference, }; - GripService.broadcast( - mealPlan.userId, - "mealPlan:itemsUpdated", - updateBroadcast, - ); + broadcastWSEvent(mealPlan.userId, "mealPlan:itemsUpdated", updateBroadcast); for (let i = 0; i < mealPlan.collaborators.length; i++) { - GripService.broadcast( + broadcastWSEvent( mealPlan.collaborators[i].id, "mealPlan:itemsUpdated", updateBroadcast, @@ -460,13 +460,9 @@ router.post( reference, }; - GripService.broadcast( - mealPlan.userId, - "mealPlan:itemsUpdated", - updateBroadcast, - ); + broadcastWSEvent(mealPlan.userId, "mealPlan:itemsUpdated", updateBroadcast); for (let i = 0; i < mealPlan.collaborators.length; i++) { - GripService.broadcast( + broadcastWSEvent( mealPlan.collaborators[i].id, "mealPlan:itemsUpdated", updateBroadcast, @@ -536,13 +532,9 @@ router.delete( reference, }; - GripService.broadcast( - mealPlan.userId, - "mealPlan:itemsUpdated", - updateBroadcast, - ); + broadcastWSEvent(mealPlan.userId, "mealPlan:itemsUpdated", updateBroadcast); for (let i = 0; i < mealPlan.collaborators.length; i++) { - GripService.broadcast( + broadcastWSEvent( mealPlan.collaborators[i].id, "mealPlan:itemsUpdated", updateBroadcast, @@ -605,6 +597,7 @@ router.get( "id", "title", "scheduled", + "scheduledDate", "meal", "createdAt", "updatedAt", @@ -648,98 +641,4 @@ router.get( }), ); -// Get ical for meal plan -router.get( - "/:mealPlanId/ical", - cors(), - wrapRequestWithErrorHandler(async (req, res) => { - const mealPlan = await MealPlan.findOne({ - where: { - id: req.params.mealPlanId, - }, - include: [ - { - model: MealPlanItem, - as: "items", - attributes: [ - "id", - "title", - "scheduled", - "meal", - "createdAt", - "updatedAt", - ], - include: [ - { - model: Recipe, - as: "recipe", - attributes: ["id", "title", "ingredients"], - }, - ], - }, - ], - }); - - if (!mealPlan) { - throw NotFound("Meal plan not found or you do not have access"); - } - - const icalEvents = mealPlan.items.map((item) => ({ - start: new Date(item.scheduled), - allDay: true, - summary: item.recipe?.title || item.title, - url: `https://recipesage.com/#/meal-planners/${mealPlan.id}`, - })); - - const mealPlanICal = ical({ - name: `RecipeSage ${mealPlan.title}`, - events: icalEvents, - }); - - res.writeHead(200, { - "Content-Type": "text/calendar; charset=utf-8", - "Content-Disposition": 'attachment; filename="calendar.ics"', - }); - - res.end(mealPlanICal.toString()); - }), -); - -// Update a meal plan meta info (NOT INCLUDING ITEMS) -// router.put( -// '/:mealPlanId', -// cors(), -// MiddlewareService.validateSession(['user']), -// MiddlewareService.validateUser, -// function(req, res) { - -// MealPlan.findOne({ -// _id: req.params.mealPlanId, -// accountId: res.locals.accountId -// }, function(err, mealPlan) { -// if (err) { -// res.status(500).json({ -// msg: "Couldn't search the database for meal plan!" -// }); -// } else if (!mealPlan) { -// res.status(404).json({ -// msg: "Meal plan with that ID does not exist or you do not have access!" -// }); -// } else { -// if (typeof req.body.title === 'string') mealPlan.title = req.body.title; -// if (req.body.collaborators) mealPlan.collaborators = req.body.collaborators; - -// mealPlan.updated = Date.now(); - -// mealPlan.save(function (err, mealPlan) { -// if (err) { -// res.status(500).send("Could not save updated meal plan!"); -// } else { -// res.status(200).json(mealPlan); -// } -// }); -// } -// }); -// }); - export default router; diff --git a/packages/backend/src/routes/shoppingLists.js b/packages/backend/src/routes/shoppingLists.js index b009fb34c..557738977 100644 --- a/packages/backend/src/routes/shoppingLists.js +++ b/packages/backend/src/routes/shoppingLists.js @@ -16,7 +16,7 @@ import { // Service import * as MiddlewareService from "../services/middleware.js"; -import * as GripService from "../services/grip.js"; +import { broadcastWSEvent } from "@recipesage/util/server/general"; import * as ShoppingListCategorizerService from "../services/shopping-list-categorizer.js"; import { joiValidator } from "../middleware/joiValidator.js"; @@ -54,18 +54,14 @@ router.post( }); for (let i = 0; i < (req.body.collaborators || []).length; i++) { - GripService.broadcast( - req.body.collaborators[i], - "shoppingList:received", - { - shoppingListId: shoppingList.id, - from: { - id: res.locals.user.id, - name: res.locals.user.name, - email: res.locals.user.email, - }, + broadcastWSEvent(req.body.collaborators[i], "shoppingList:received", { + shoppingListId: shoppingList.id, + from: { + id: res.locals.user.id, + name: res.locals.user.name, + email: res.locals.user.email, }, - ); + }); } res.status(200).json(shoppingList); @@ -197,13 +193,13 @@ router.post( reference, }; - GripService.broadcast( + broadcastWSEvent( shoppingList.userId, "shoppingList:itemsUpdated", broadcastPayload, ); for (let i = 0; i < shoppingList.collaborators.length; i++) { - GripService.broadcast( + broadcastWSEvent( shoppingList.collaborators[i].id, "shoppingList:itemsUpdated", broadcastPayload, @@ -248,7 +244,7 @@ router.delete( await shoppingList.destroy(); for (let i = 0; i < (shoppingList.collaborators || []).length; i++) { - GripService.broadcast( + broadcastWSEvent( shoppingList.collaborators[i], "shoppingList:removed", { @@ -320,13 +316,13 @@ router.delete( reference, }; - GripService.broadcast( + broadcastWSEvent( shoppingList.userId, "shoppingList:itemsUpdated", deletedItemBroadcast, ); for (let i = 0; i < shoppingList.collaborators.length; i++) { - GripService.broadcast( + broadcastWSEvent( shoppingList.collaborators[i].id, "shoppingList:itemsUpdated", deletedItemBroadcast, @@ -511,13 +507,13 @@ router.put( reference, }; - GripService.broadcast( + broadcastWSEvent( shoppingList.userId, "shoppingList:itemsUpdated", broadcast, ); for (let i = 0; i < shoppingList.collaborators.length; i++) { - GripService.broadcast( + broadcastWSEvent( shoppingList.collaborators[i].id, "shoppingList:itemsUpdated", broadcast, diff --git a/packages/backend/src/routes/ws.js b/packages/backend/src/routes/ws.js deleted file mode 100644 index 1a2bd1d56..000000000 --- a/packages/backend/src/routes/ws.js +++ /dev/null @@ -1,54 +0,0 @@ -import * as express from "express"; -const router = express.Router(); - -import * as MiddlewareService from "../services/middleware.js"; -import * as GripService from "../services/grip.js"; - -router.use(GripService.expressGrip.preHandlerGripMiddleware); - -router.all( - "/", - MiddlewareService.validateSession(["user"]), - function (req, res, next) { - // Reject non-WebSocket requests - if (!GripService.expressGrip.verifyIsWebSocket(res, next)) { - return; - } - - const ws = GripService.expressGrip.getWsContext(res); - - // If this is a new connection, accept it and subscribe it to a channel - if (ws.isOpening()) { - ws.accept(); - ws.subscribe("all"); - ws.subscribe(res.locals.session.userId); - } - - while (ws.canRecv()) { - let message; - - try { - message = ws.recv(); - } catch (e) { - if (e.message != "Client disconnected unexpectedly.") throw e; - } - - // If return value is null then connection is closed - if (message == null) { - ws.close(); - break; - } - - // Echo the message - ws.send(message); - } - - // next() must be called for the post-handler middleware to execute - next(); - }, -); - -// Add the post-handler middleware to the back of the stack -router.use(GripService.expressGrip.postHandlerGripMiddleware); - -export default router; diff --git a/packages/backend/src/services/grip.js b/packages/backend/src/services/grip.js deleted file mode 100644 index ef2be7965..000000000 --- a/packages/backend/src/services/grip.js +++ /dev/null @@ -1,26 +0,0 @@ -import * as grip from "grip"; -import * as expressGrip from "express-grip"; - -expressGrip.configure({ - gripProxies: [ - // pushpin config - { - control_uri: process.env.GRIP_URL, - key: process.env.GRIP_KEY, - }, - ], -}); - -const broadcast = function (channel, type, data) { - const body = { - type: type, - data: data || {}, - }; - - expressGrip.publish( - channel, - new grip.WebSocketMessageFormat(JSON.stringify(body)), - ); -}; - -export { expressGrip, broadcast }; diff --git a/packages/backend/src/services/util.js b/packages/backend/src/services/util.js index b82de84e3..006717ce1 100644 --- a/packages/backend/src/services/util.js +++ b/packages/backend/src/services/util.js @@ -4,7 +4,7 @@ import * as zlib from "zlib"; // Service import * as FirebaseService from "./firebase.js"; -import * as GripService from "./grip.js"; +import * as ServerUtil from "@recipesage/util/server/general"; /** * DO NOT ADD ANYTHING TO THIS FILE @@ -40,7 +40,7 @@ export const dispatchImportNotification = (user, status, reason) => { ); } - sendQueues.push(GripService.broadcast(user.id, type, message)); + sendQueues.push(ServerUtil.broadcastWSEvent(user.id, type, message)); return Promise.all(sendQueues); }; @@ -101,7 +101,9 @@ export const dispatchMessageNotification = (user, fullMessage) => { ); } - sendQueues.push(GripService.broadcast(user.id, "messages:new", message)); + sendQueues.push( + ServerUtil.broadcastWSEvent(user.id, "messages:new", message), + ); return Promise.all(sendQueues); }; diff --git a/packages/backend/src/services/util.spec.js b/packages/backend/src/services/util.spec.js index 0c5dc3298..84900d462 100644 --- a/packages/backend/src/services/util.spec.js +++ b/packages/backend/src/services/util.spec.js @@ -1,22 +1,16 @@ import { expect } from "chai"; -import * as sinon from "sinon"; import * as path from "path"; -import { setup, cleanup, randomString } from "../testutils.js"; +import { setup, cleanup } from "../testutils.js"; import { validatePassword, validateEmail, sanitizeEmail, - dispatchImportNotification, - dispatchMessageNotification, findFilesByRegex, } from "../services/util.js"; -import * as FirebaseService from "../services/firebase.js"; -import * as GripService from "../services/grip.js"; - describe("utils", () => { beforeAll(async () => { await setup(); @@ -115,244 +109,4 @@ describe("utils", () => { expect(files).to.have.length(0); }); }); - - describe("dispatchImportNotification", () => { - let fcmTokens, fcmSendMessagesStub, gripBroadcastStub; - - beforeEach(() => { - fcmTokens = [ - { - id: "a", - token: "token1", - }, - { - id: "b", - token: "token2", - }, - ]; - - fcmSendMessagesStub = sinon - .stub(FirebaseService, "sendMessages") - .returns(Promise.resolve()); - gripBroadcastStub = sinon - .stub(GripService, "broadcast") - .returns(Promise.resolve()); - }); - - afterEach(() => { - fcmSendMessagesStub.restore(); - gripBroadcastStub.restore(); - }); - - it("accepts status 0 (complete)", async () => { - await dispatchImportNotification({ fcmTokens }, 0, "anyreason"); - - expect(fcmSendMessagesStub.getCalls()[0].args[1].type).to.equal( - "import:pepperplate:complete", - ); - expect(gripBroadcastStub.getCalls()[0].args[1]).to.equal( - "import:pepperplate:complete", - ); - }); - - it("accepts status 1 (failed)", async () => { - await dispatchImportNotification({ fcmTokens }, 1, "anyreason"); - - expect(fcmSendMessagesStub.getCalls()[0].args[1].type).to.equal( - "import:pepperplate:failed", - ); - expect(gripBroadcastStub.getCalls()[0].args[1]).to.equal( - "import:pepperplate:failed", - ); - }); - - it("accepts status 2 (working, progress)", async () => { - await dispatchImportNotification({ fcmTokens }, 2, "anyreason"); - - expect(fcmSendMessagesStub.getCalls()[0].args[1].type).to.equal( - "import:pepperplate:working", - ); - expect(gripBroadcastStub.getCalls()[0].args[1]).to.equal( - "import:pepperplate:working", - ); - }); - - it("rejects status higher than 2", async () => { - await dispatchImportNotification({ fcmTokens }, 3, "anyreason"); - - expect(fcmSendMessagesStub.getCalls()).to.have.length(0); - expect(gripBroadcastStub.getCalls()).to.have.length(0); - }); - - it("passes reason in message", async () => { - let reason = "myreasonhere"; - await dispatchImportNotification({ fcmTokens }, 0, reason); - - expect(fcmSendMessagesStub.getCalls()[0].args[1].reason).to.equal(reason); - expect(gripBroadcastStub.getCalls()[0].args[2].reason).to.equal(reason); - }); - - it("calls with an array of fcmTokens", async () => { - await dispatchImportNotification({ fcmTokens }, 0, "anyreason"); - - expect(fcmSendMessagesStub.getCalls()[0].args[0][0]).to.equal( - fcmTokens[0].token, - ); - expect(fcmSendMessagesStub.getCalls()[0].args[0][1]).to.equal( - fcmTokens[1].token, - ); - }); - }); - - describe("dispatchMessageNotification", () => { - let userId, fcmTokens, message, fcmSendMessagesStub, gripBroadcastStub; - - beforeAll(async () => { - userId = "15"; - fcmTokens = [ - { - id: "a", - token: "token1", - }, - { - id: "b", - token: "token2", - }, - ]; - message = { - id: "22", - body: randomString(2000), - otherUser: { - name: "test1", - }, - fromUser: { - name: "test1", - }, - toUser: { - name: "test2", - }, - recipe: { - id: "44", - title: "recipeTitle", - images: [ - { - location: "location", - }, - ], - }, - }; - - fcmSendMessagesStub = sinon - .stub(FirebaseService, "sendMessages") - .returns(Promise.resolve()); - gripBroadcastStub = sinon - .stub(GripService, "broadcast") - .returns(Promise.resolve()); - - await dispatchMessageNotification({ id: userId, fcmTokens }, message); - }); - - afterAll(() => { - fcmSendMessagesStub.restore(); - gripBroadcastStub.restore(); - }); - - describe("fcm", () => { - it("calls fcm sendMessages", () => { - sinon.assert.calledOnce(fcmSendMessagesStub); - }); - - it("sends to all passed tokens", () => { - expect(fcmSendMessagesStub.getCalls()[0].args[0][0]).to.equal( - fcmTokens[0].token, - ); - expect(fcmSendMessagesStub.getCalls()[0].args[0][1]).to.equal( - fcmTokens[1].token, - ); - }); - - it("sends with correct type", () => { - expect(fcmSendMessagesStub.getCalls()[0].args[1].type).to.equal( - "messages:new", - ); - }); - - it("sends with correct message", () => { - let stubMessageCall = fcmSendMessagesStub.getCalls()[0].args[1].message; - expect(typeof stubMessageCall).to.equal("string"); - - let parsedMessage = JSON.parse(stubMessageCall); - - expect(parsedMessage.id).to.equal("22"); - expect(parsedMessage.body).to.equal(message.body.substring(0, 1000)); - expect(parsedMessage.otherUser.name).to.equal("test1"); - expect(parsedMessage.fromUser.name).to.equal("test1"); - expect(parsedMessage.toUser.name).to.equal("test2"); - expect(parsedMessage.recipe.id).to.equal("44"); - expect(parsedMessage.recipe.title).to.equal("recipeTitle"); - expect(parsedMessage.recipe.images[0].location).to.equal("location"); - }); - - it("sends no additional fields", () => { - let stubMessageCall = fcmSendMessagesStub.getCalls()[0].args[1]; - expect(Object.keys(stubMessageCall)).to.have.length(2); - - let parsedMessage = JSON.parse(stubMessageCall.message); - expect(Object.keys(parsedMessage)).to.have.length(6); - expect(Object.keys(parsedMessage.recipe)).to.have.length(3); - expect(Object.keys(parsedMessage.recipe.images)).to.have.length(1); - }); - }); - - describe("grip", () => { - it("calls grip broadcast", () => { - sinon.assert.calledOnce(gripBroadcastStub); - }); - - it("sends to user channel", () => { - expect(gripBroadcastStub.getCalls()[0].args[0]).to.equal(userId); - }); - - it("sends with correct type", () => { - expect(gripBroadcastStub.getCalls()[0].args[1]).to.equal( - "messages:new", - ); - }); - - it("sends with correct message", () => { - let stubMessageCall = gripBroadcastStub.getCalls()[0].args[2]; - - // Message should be immutable - expect(stubMessageCall).to.not.equal(message); - expect(stubMessageCall.id).to.equal(message.id); - expect(stubMessageCall.body).to.equal(message.body.substring(0, 1000)); - - // Users should be mutable - expect(stubMessageCall.otherUser).to.equal(message.otherUser); - expect(stubMessageCall.fromUser).to.equal(message.fromUser); - expect(stubMessageCall.toUser).to.equal(message.toUser); - - // Recipe should be immutable - expect(stubMessageCall.recipe).to.not.equal(message.recipe); - expect(stubMessageCall.recipe.id).to.equal(message.recipe.id); - expect(stubMessageCall.recipe.title).to.equal(message.recipe.title); - - // Recipe image should be immutable - expect(stubMessageCall.recipe.images).to.not.equal( - message.recipe.images, - ); - expect(stubMessageCall.recipe.images[0].location).to.equal( - message.recipe.images[0].location, - ); - }); - - it("sends no additional fields", () => { - let stubMessageCall = gripBroadcastStub.getCalls()[0].args[2]; - expect(Object.keys(stubMessageCall)).to.have.length(6); - expect(Object.keys(stubMessageCall.recipe)).to.have.length(3); - expect(stubMessageCall.recipe.images).to.have.length(1); - expect(Object.keys(stubMessageCall.recipe.images[0])).to.have.length(1); - }); - }); - }); }); diff --git a/packages/express/src/defineHandler.ts b/packages/express/src/defineHandler.ts new file mode 100644 index 000000000..ffdb3fe27 --- /dev/null +++ b/packages/express/src/defineHandler.ts @@ -0,0 +1,117 @@ +import { NextFunction, Request, Response } from "express"; +import { ZodError, ZodSchema } from "zod"; +import { + BadRequestError, + InternalServerError, + ServerError, + UnauthorizedError, +} from "./errors"; +import { logError } from "./logError"; +import { + validateSession, + extendSession, +} from "@recipesage/util/server/general"; +import { Session } from "@prisma/client"; + +export enum AuthenticationEnforcement { + Required = "required", + Optional = "optional", + None = "none", +} +type SessionPresent = { + [AuthenticationEnforcement.Required]: Session; + [AuthenticationEnforcement.Optional]: Session | undefined; + [AuthenticationEnforcement.None]: undefined; +}; +type Zodifiable = { + params?: ZodSchema

; + query?: ZodSchema; + body?: ZodSchema; + response?: ZodSchema; +}; +export const defineHandler = < + GParams, + GQuery, + GBody, + GResponse, + GAuthentication extends AuthenticationEnforcement, +>( + opts: { + schema: Zodifiable; + authentication: GAuthentication; + }, + handler: ( + req: Request, + res: Response< + GResponse, + { + session: SessionPresent[GAuthentication]; + } + >, + next: NextFunction, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ) => any, +) => { + return async (req: Request, res: Response, next: NextFunction) => { + try { + try { + opts.schema.params?.parse(req.params); + opts.schema.query?.parse(req.params); + opts.schema.body?.parse(req.body); + } catch (e) { + if (e instanceof ZodError) { + throw new BadRequestError(e.message); + } + throw new InternalServerError("Unknown error parsing request"); + } + + let session: Session | undefined; + if (opts.authentication !== AuthenticationEnforcement.None) { + const authorization = req.headers.authorization; + if ( + opts.authentication === AuthenticationEnforcement.Required && + !authorization + ) { + throw new UnauthorizedError("You must pass an authorization header"); + } + + if (authorization) { + const authorizationParts = authorization.split(" "); + const token = authorizationParts.at(1); + session = token ? await validateSession(token) : undefined; + if (session) extendSession(session); + + if ( + opts.authentication === AuthenticationEnforcement.Required && + !session + ) { + throw new UnauthorizedError("Token is not valid"); + } + } + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const response = await handler(req as any, res as any, next); + + res.status(200).send(response); + } catch (e) { + if (e instanceof ServerError) { + logError(e); + + if (process.env.NODE_ENV !== "production") { + res.status(e.status).send(e); + } else { + res.status(e.status).send(e.name); + } + } else { + logError(e); + + if (process.env.NODE_ENV !== "production") { + res.status(500).send(e); + } else { + res.status(500).send("Internal server error"); + } + } + } + }; +}; diff --git a/packages/express/src/logError.ts b/packages/express/src/logError.ts new file mode 100644 index 000000000..23d7bc919 --- /dev/null +++ b/packages/express/src/logError.ts @@ -0,0 +1,18 @@ +import { ServerError } from "./errors"; +import * as Sentry from "@sentry/node"; + +export const logError = (e: unknown) => { + console.error(e); + + let status; + if (e instanceof ServerError) { + status = e.status; + } else { + status = 500; + } + + const isExpectedError = status < 500 || status > 599; + if (isExpectedError) return; + + Sentry.captureException(e); +}; diff --git a/packages/express/src/routes/index.ts b/packages/express/src/routes/index.ts index 92e09c0f6..71c38cf5b 100644 --- a/packages/express/src/routes/index.ts +++ b/packages/express/src/routes/index.ts @@ -1,8 +1,12 @@ import * as express from "express"; import { importRouter } from "./import"; +import { mealPlansRouter } from "./mealPlans"; +import { wsRouter } from "./ws"; const router = express.Router(); router.use("/import", importRouter); +router.use("/mealplans", mealPlansRouter); +router.use("/ws", wsRouter); export { router as typesafeExpressIndexRouter }; diff --git a/packages/express/src/routes/mealPlans/ical.ts b/packages/express/src/routes/mealPlans/ical.ts new file mode 100644 index 000000000..cb7ee16ee --- /dev/null +++ b/packages/express/src/routes/mealPlans/ical.ts @@ -0,0 +1,74 @@ +import ical from "ical-generator"; +import { prisma } from "@recipesage/prisma"; +import { z } from "zod"; +import { convertPrismaDateToDatestamp } from "@recipesage/util/server/db"; +import { NotFoundError } from "../../errors"; +import { AuthenticationEnforcement, defineHandler } from "../../defineHandler"; + +const schema = { + params: z.object({ + mealPlanId: z.string(), + }), +}; + +export const mealPlansIcalHandler = defineHandler( + { + schema, + authentication: AuthenticationEnforcement.None, + }, + async (req, res) => { + const mealPlan = await prisma.mealPlan.findUnique({ + where: { + id: req.params.mealPlanId, + }, + select: { + id: true, + title: true, + items: { + select: { + id: true, + title: true, + scheduled: true, + scheduledDate: true, + meal: true, + createdAt: true, + updatedAt: true, + recipeId: true, + recipe: { + select: { + id: true, + title: true, + ingredients: true, + }, + }, + }, + }, + }, + }); + + if (!mealPlan) { + throw new NotFoundError("Meal plan not found or you do not have access"); + } + + const icalEvents = mealPlan.items.map((item) => ({ + start: + item.scheduled || + convertPrismaDateToDatestamp(item, "scheduledDate").scheduledDate, + allDay: true, + summary: item.recipe?.title || item.title, + url: `https://recipesage.com/#/meal-planners/${mealPlan.id}`, + })); + + const mealPlanICal = ical({ + name: `RecipeSage ${mealPlan.title}`, + events: icalEvents, + }); + + res.writeHead(200, { + "Content-Type": "text/calendar; charset=utf-8", + "Content-Disposition": 'attachment; filename="calendar.ics"', + }); + + res.end(mealPlanICal.toString()); + }, +); diff --git a/packages/express/src/routes/mealPlans/index.ts b/packages/express/src/routes/mealPlans/index.ts new file mode 100644 index 000000000..401db1518 --- /dev/null +++ b/packages/express/src/routes/mealPlans/index.ts @@ -0,0 +1,8 @@ +import * as express from "express"; +import { mealPlansIcalHandler } from "./ical"; + +const router = express.Router(); + +router.get("/:mealPlanId/ical", mealPlansIcalHandler); + +export { router as mealPlansRouter }; diff --git a/packages/express/src/routes/ws/index.ts b/packages/express/src/routes/ws/index.ts new file mode 100644 index 000000000..1f55142d3 --- /dev/null +++ b/packages/express/src/routes/ws/index.ts @@ -0,0 +1,61 @@ +import * as express from "express"; +const router = express.Router(); + +import { serveGrip, validateSession } from "@recipesage/util/server/general"; + +router.use(serveGrip); + +router.all( + "/", + // eslint-disable-next-line @typescript-eslint/no-explicit-any + async function (req: any, res) { + if (!req.grip.isProxied) { + return; + } + + const ws = req.grip.wsContext; + if (!ws) { + res.status(400).send("Not a grip WS request"); + return; + } + + const session = await validateSession(req.query.token); + if (!session) { + res.status(401).send("Unauthorized"); + ws.close(); + return; + } + + // If this is a new connection, accept it and subscribe it to a channel + if (ws.isOpening()) { + ws.accept(); + ws.subscribe("all"); + ws.subscribe(session.userId); + } + + while (ws.canRecv()) { + let message; + + try { + message = ws.recv(); + } catch (e) { + if ( + e instanceof Error && + e.message == "Client disconnected unexpectedly." + ) { + // Do nothing + } else throw e; + } + + // If return value is null then connection is closed + if (message == null) { + ws.close(); + break; + } + } + + res.end(); + }, +); + +export { router as wsRouter }; diff --git a/packages/express/src/zodify.ts b/packages/express/src/zodify.ts new file mode 100644 index 000000000..164857905 --- /dev/null +++ b/packages/express/src/zodify.ts @@ -0,0 +1,26 @@ +import { NextFunction, Request, Response } from "express"; +import { ZodSchema } from "zod"; + +type Zodifiable = { + params?: ZodSchema

; + query?: ZodSchema; + body?: ZodSchema; + response?: ZodSchema; +}; +export const zodify = ( + schema: Zodifiable, + handler: ( + req: Request, + res: Response, + next: NextFunction, + ) => void, +) => { + return (req: Request, res: Response, next: NextFunction) => { + schema.params?.parse(req.params); + schema.query?.parse(req.query); + schema.body?.parse(req.body); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return handler(req as any, res, next); + }; +}; diff --git a/packages/frontend/src/app/components/meal-calendar/calendar-item/calendar-item.component.ts b/packages/frontend/src/app/components/meal-calendar/calendar-item/calendar-item.component.ts index 47ff2b2af..8e5545b25 100644 --- a/packages/frontend/src/app/components/meal-calendar/calendar-item/calendar-item.component.ts +++ b/packages/frontend/src/app/components/meal-calendar/calendar-item/calendar-item.component.ts @@ -1,5 +1,5 @@ import { Component, Input } from "@angular/core"; -import { MealPlanItem } from "../../../services/meal-plan.service"; +import { MealPlanItemSummary } from "@recipesage/prisma"; @Component({ selector: "calendar-item", @@ -10,7 +10,7 @@ export class CalendarItemComponent { @Input({ required: true, }) - mealItem!: MealPlanItem; + mealItem!: MealPlanItemSummary; constructor() {} } diff --git a/packages/frontend/src/app/components/meal-calendar/meal-calendar.component.ts b/packages/frontend/src/app/components/meal-calendar/meal-calendar.component.ts index 4db3148aa..2f4c5548a 100644 --- a/packages/frontend/src/app/components/meal-calendar/meal-calendar.component.ts +++ b/packages/frontend/src/app/components/meal-calendar/meal-calendar.component.ts @@ -9,6 +9,7 @@ import { MealPlan, MealPlanItem, } from "../../services/meal-plan.service"; +import { MealPlanItemSummary, MealPlanSummary } from "@recipesage/prisma"; @Component({ selector: "meal-calendar", @@ -16,31 +17,31 @@ import { styleUrls: ["./meal-calendar.component.scss"], }) export class MealCalendarComponent { - private _mealPlan!: MealPlan; + private _mealPlanItems!: MealPlanItemSummary[]; @Input({ required: true, }) - set mealPlan(mealPlan: MealPlan) { - this._mealPlan = mealPlan; + set mealPlanItems(mealPlanItems: MealPlanItemSummary[]) { + this._mealPlanItems = mealPlanItems; this.processIncomingMealPlan(); } - get mealPlan() { - return this._mealPlan; + get mealPlanItems() { + return this._mealPlanItems; } @Input() enableEditing = false; @Input() mode = "outline"; - @Output() mealsByDateChange = new EventEmitter(); + @Output() mealsByDateChange = new EventEmitter(); _mealsByDate: { [year: number]: { [month: number]: { [day: number]: { itemsByMeal: { - [key in MealName]: MealPlanItem[]; + [key in MealName]: MealPlanItemSummary[]; }; - items: MealPlanItem[]; + items: MealPlanItemSummary[]; meals: MealName[]; }; }; @@ -62,13 +63,13 @@ export class MealCalendarComponent { center: Date = new Date(this.today); dayTitles?: string[]; - @Output() selectedDaysChange = new EventEmitter(); + @Output() selectedDaysChange = new EventEmitter(); @Output() itemMoved = new EventEmitter(); @Output() itemClicked = new EventEmitter(); @Output() dayClicked = new EventEmitter(); - private _selectedDays: number[] = [this.getToday().getTime()]; + private _selectedDays: string[] = [this.getToday()]; highlightedDay?: Dayjs; dayDragInProgress = false; @@ -144,9 +145,8 @@ export class MealCalendarComponent { return new Date(this.center.getFullYear(), newMonth, 1); } - getToday(): Date { - const now = new Date(); - return new Date(now.getFullYear(), now.getMonth(), now.getDate()); + getToday(): string { + return dayjs().format("YYYY-MM-DD"); } // Moves the calendar. Positive = next month, negative = last month @@ -158,7 +158,7 @@ export class MealCalendarComponent { dayjs(this.selectedDays[0]).isBefore(bounds[0]) || dayjs(this.selectedDays[0]).isAfter(bounds[1]) ) { - this.selectedDays = [this.center.getTime()]; + this.selectedDays = [dayjs(this.center).format("YYYY-MM-DD")]; } } @@ -175,6 +175,22 @@ export class MealCalendarComponent { return date.toLocaleString(window.navigator.language, { month: "long" }); } + getYMD(stamp: string | Date | Dayjs) { + let dateString; + if (typeof stamp === "string") { + dateString = stamp; + } else { + const scheduledDayjs = dayjs(stamp); + dateString = `${scheduledDayjs.year()}-${scheduledDayjs.month() + 1}-${scheduledDayjs.date()}`; + } + + const [year, month, day] = dateString + .split("-") + .map((el) => parseInt(el, 10)); + + return [year, month, day]; + } + processIncomingMealPlan() { this.mealsByDate = {}; @@ -185,7 +201,7 @@ export class MealCalendarComponent { snacks: 4, other: 5, }; - this.mealPlan.items + this.mealPlanItems .sort((a, b) => { const comp = (mealSortOrder[a.meal as keyof typeof mealSortOrder] || 6) - @@ -194,23 +210,25 @@ export class MealCalendarComponent { return comp; }) .forEach((item) => { - const day = dayjs(item.scheduled); - this.mealsByDate[day.year()] = this.mealsByDate[day.year()] || {}; - this.mealsByDate[day.year()][day.month()] = - this.mealsByDate[day.year()][day.month()] || {}; - const dayData = (this.mealsByDate[day.year()][day.month()][day.date()] = - this.mealsByDate[day.year()][day.month()][day.date()] || { - itemsByMeal: { - breakfast: [], - lunch: [], - dinner: [], - snacks: [], - other: [], - }, - items: [], - meals: Object.values(MealName), - }); - console.log(dayData, day.year(), day.month(), day.date()); + const [year, month, day] = this.getYMD( + item.scheduled || item.scheduledDate, + ); + this.mealsByDate[year] = this.mealsByDate[year] || {}; + this.mealsByDate[year][month] = this.mealsByDate[year][month] || {}; + const dayData = (this.mealsByDate[year][month][day] = this.mealsByDate[ + year + ][month][day] || { + itemsByMeal: { + breakfast: [], + lunch: [], + dinner: [], + snacks: [], + other: [], + }, + items: [], + meals: Object.values(MealName), + }); + console.log(dayData, year, month, day); dayData.itemsByMeal[ item.meal as keyof typeof dayData.itemsByMeal ]?.push(item); @@ -218,19 +236,19 @@ export class MealCalendarComponent { }); } - mealItemsByDay(date: Dayjs | Date | string | number) { - const day = dayjs(date); + mealItemsByDay(dateStamp: string | Date | Dayjs) { + const [year, month, day] = this.getYMD(dateStamp); return ( - this.mealsByDate[day.year()]?.[day.month()]?.[day.date()] || { + this.mealsByDate[year]?.[month]?.[day] || { meals: [], items: [], } ); } - mealItemTitlesByDay(date: Date | string | number) { - const mealItems = this.mealItemsByDay(date); - return mealItems.items.map((item: MealPlanItem) => item.title); + mealItemTitlesByDay(dateStamp: string) { + const mealItems = this.mealItemsByDay(dateStamp); + return mealItems.items.map((item) => item.title); } formatItemCreationDate(date: Date | string | number) { @@ -238,7 +256,7 @@ export class MealCalendarComponent { } isSelected(day: Dayjs) { - return this.selectedDays.includes(day.toDate().getTime()); + return this.selectedDays.includes(day.format("YYYY-MM-DD")); } dayKeyEnter(event: any, day: Dayjs) { @@ -249,34 +267,31 @@ export class MealCalendarComponent { dayMouseDown(event: any, day: Dayjs) { this.dayDragInProgress = true; if (event.shiftKey) - this.selectedDays = this.getDaysBetween( - this.selectedDays[0], - day.valueOf(), - ); - else this.selectedDays = [day.toDate().getTime()]; + this.selectedDays = this.getDaysBetween(this.selectedDays[0], day); + else this.selectedDays = [day.format("YYYY-MM-DD")]; this.dayClicked.emit(day.toDate()); } - getDaysBetween(day1: number, day2: number): number[] { - const days: number[] = []; + getDaysBetween( + dateStamp1: string | Dayjs, + dateStamp2: string | Dayjs, + ): string[] { + const dateStamps: string[] = []; - const iterDate = new Date(day1); + let iterDate = dayjs(dateStamp1); - while (iterDate <= new Date(day2)) { - days.push(iterDate.getTime()); + while (iterDate <= dayjs(dateStamp2)) { + dateStamps.push(iterDate.format("YYYY-MM-DD")); - iterDate.setDate(iterDate.getDate() + 1); + iterDate = dayjs(iterDate).add(1, "day"); } - return days; + return dateStamps; } dayMouseOver(_: any, day: Dayjs) { if (this.dayDragInProgress) { - this.selectedDays = this.getDaysBetween( - this.selectedDays[0], - day.valueOf(), - ); + this.selectedDays = this.getDaysBetween(this.selectedDays[0], day); } } @@ -289,22 +304,16 @@ export class MealCalendarComponent { this.dayDragInProgress = false; this.highlightedDay = undefined; const mealItemId = event.dataTransfer.getData("text"); - const mealItem = this.mealPlan.items.find((item) => item.id === mealItemId); + const mealItem = this.mealPlanItems.find((item) => item.id === mealItemId); if (!mealItem) return; - const currDate = new Date(mealItem.scheduled); - const newDate = day.toDate(); + const newDate = day.format("YYYY-MM-DD"); // Do not trigger event if the item has not moved to a different day - if ( - currDate.getFullYear() === newDate.getFullYear() && - currDate.getMonth() === newDate.getMonth() && - currDate.getDate() === newDate.getDate() - ) - return; + if (mealItem.scheduledDate === newDate) return; this.itemMoved.emit({ mealItem, - day: day.toString(), + dateStamp: newDate, }); } diff --git a/packages/frontend/src/app/components/meal-calendar/meal-group/meal-group.component.ts b/packages/frontend/src/app/components/meal-calendar/meal-group/meal-group.component.ts index a003445e6..d6e9f7ea6 100644 --- a/packages/frontend/src/app/components/meal-calendar/meal-group/meal-group.component.ts +++ b/packages/frontend/src/app/components/meal-calendar/meal-group/meal-group.component.ts @@ -1,5 +1,6 @@ import { Component, Input, Output, EventEmitter } from "@angular/core"; import { MealName, MealPlanItem } from "../../../services/meal-plan.service"; +import { MealPlanItemSummary } from "@recipesage/prisma"; @Component({ selector: "meal-group", @@ -12,7 +13,7 @@ export class MealGroupComponent { }) mealItems!: { meals: MealName[]; - itemsByMeal: Record; + itemsByMeal: Record; }; @Input() enableEditing: boolean = false; @@ -23,7 +24,7 @@ export class MealGroupComponent { constructor() {} - dragStart(event: any, mealItem: MealPlanItem) { + dragStart(event: any, mealItem: MealPlanItemSummary) { this.mealItemsDragging[mealItem.id] = true; event.dataTransfer.setData("text", mealItem.id); // Must set 'text' prop for Android dragndrop, otherwise evt will be cancelled } diff --git a/packages/frontend/src/app/pages/home/home.page.ts b/packages/frontend/src/app/pages/home/home.page.ts index 0b6da1482..32e4e4818 100644 --- a/packages/frontend/src/app/pages/home/home.page.ts +++ b/packages/frontend/src/app/pages/home/home.page.ts @@ -312,7 +312,7 @@ export class HomePage { const sortPreference = this.preferences[MyRecipesPreferenceKey.SortBy]; const result = await this.trpcService.handle( - this.trpcService.trpc.getRecipes.query({ + this.trpcService.trpc.recipes.getRecipes.query({ folder: this.folder, orderBy: sortPreference.replace("-", "") as | "title" @@ -460,7 +460,7 @@ export class HomePage { const result = await this.trpcService .handle( - this.trpcService.trpc.searchRecipes.query({ + this.trpcService.trpc.recipes.searchRecipes.query({ searchTerm: text, folder: this.folder, labels: this.selectedLabels.length ? this.selectedLabels : undefined, diff --git a/packages/frontend/src/app/pages/meal-plan-components/meal-plan-bulk-pin-modal/index.ts b/packages/frontend/src/app/pages/meal-plan-components/meal-plan-bulk-pin-modal/index.ts index 7078711a4..647548729 100644 --- a/packages/frontend/src/app/pages/meal-plan-components/meal-plan-bulk-pin-modal/index.ts +++ b/packages/frontend/src/app/pages/meal-plan-components/meal-plan-bulk-pin-modal/index.ts @@ -10,6 +10,7 @@ import { RecipeService } from "~/services/recipe.service"; import { LoadingService } from "~/services/loading.service"; import { CookingToolbarService } from "~/services/cooking-toolbar.service"; import { UtilService } from "~/services/util.service"; +import { MealPlanItemSummary } from "@recipesage/prisma"; @Component({ selector: "page-meal-plan-bulk-pin-modal", @@ -20,7 +21,7 @@ export class MealPlanBulkPinModalPage { @Input({ required: true, }) - mealItems!: MealPlanItem[]; + mealItems!: MealPlanItemSummary[]; allSelected = true; recipeIdSelectionMap: Record = {}; @@ -67,7 +68,7 @@ export class MealPlanBulkPinModalPage { this.cookingToolbarService.pinRecipe({ id: mealItem.recipe.id, title: mealItem.recipe.title, - imageUrl: mealItem.recipe.images[0]?.location, + imageUrl: mealItem.recipe.recipeImages.at(0)?.image.location, }); } }); diff --git a/packages/frontend/src/app/pages/meal-plan-components/meal-plan-item-details-modal/meal-plan-item-details-modal.page.html b/packages/frontend/src/app/pages/meal-plan-components/meal-plan-item-details-modal/meal-plan-item-details-modal.page.html index da80d2b24..3b1165a87 100644 --- a/packages/frontend/src/app/pages/meal-plan-components/meal-plan-item-details-modal/meal-plan-item-details-modal.page.html +++ b/packages/frontend/src/app/pages/meal-plan-components/meal-plan-item-details-modal/meal-plan-item-details-modal.page.html @@ -14,8 +14,8 @@ - - + +

{{ mealItem.title }}

@@ -23,14 +23,15 @@

{{ mealItem.title }}

{{ 'pages.mealPlanItemDetailsModal.scheduled' | - translate:{meal:mealItem.meal,date:formatDate(mealItem.scheduled)} }} + translate:{meal:mealItem.meal,date:formatDate(mealItem.scheduledDate)} + }}

{{ 'pages.mealPlanItemDetailsModal.lastModified' | - translate:{name:mealItem.owner.name,date:formatDate(mealItem.updatedAt)} + translate:{name:mealItem.user.name,date:formatDate(mealItem.updatedAt)} }}

diff --git a/packages/frontend/src/app/pages/meal-plan-components/meal-plan-item-details-modal/meal-plan-item-details-modal.page.ts b/packages/frontend/src/app/pages/meal-plan-components/meal-plan-item-details-modal/meal-plan-item-details-modal.page.ts index a1f64b807..b3c66f50a 100644 --- a/packages/frontend/src/app/pages/meal-plan-components/meal-plan-item-details-modal/meal-plan-item-details-modal.page.ts +++ b/packages/frontend/src/app/pages/meal-plan-components/meal-plan-item-details-modal/meal-plan-item-details-modal.page.ts @@ -17,6 +17,8 @@ import { NewMealPlanItemModalPage } from "../new-meal-plan-item-modal/new-meal-p import { AddRecipeToShoppingListModalPage } from "~/pages/recipe-components/add-recipe-to-shopping-list-modal/add-recipe-to-shopping-list-modal.page"; import dayjs from "dayjs"; +import { MealPlanItemSummary } from "@recipesage/prisma"; +import { TRPCService } from "../../../services/trpc.service"; @Component({ selector: "page-meal-plan-item-details-modal", @@ -31,19 +33,17 @@ export class MealPlanItemDetailsModalPage { @Input({ required: true, }) - mealItem!: MealPlanItem; + mealItem!: MealPlanItemSummary; constructor( - public navCtrl: NavController, - public translate: TranslateService, - public modalCtrl: ModalController, - public alertCtrl: AlertController, - public mealPlanService: MealPlanService, + private navCtrl: NavController, + private translate: TranslateService, + private modalCtrl: ModalController, + private alertCtrl: AlertController, + private trpcService: TRPCService, public cookingToolbarService: CookingToolbarService, - public recipeService: RecipeService, - public loadingService: LoadingService, - public utilService: UtilService, - public toastCtrl: ToastController, + private recipeService: RecipeService, + private loadingService: LoadingService, ) {} openRecipe() { @@ -63,7 +63,9 @@ export class MealPlanItemDetailsModalPage { inputType: this.mealItem.recipe ? "recipe" : "manualEntry", title: this.mealItem.title, recipe: this.mealItem.recipe, - scheduled: this.mealItem.scheduled, + scheduledDate: this.mealItem.scheduled + ? dayjs(this.mealItem.scheduled).format("YYYY-MM-DD") + : this.mealItem.scheduledDate, meal: this.mealItem.meal, }, }); @@ -75,19 +77,17 @@ export class MealPlanItemDetailsModalPage { const loading = this.loadingService.start(); - const response = await this.mealPlanService.updateItems(this.mealPlanId, { - items: [ - { - id: this.mealItem.id, - title: item.title, - recipeId: item.recipeId, - scheduled: item.scheduled, - meal: item.meal, - }, - ], - }); + const result = await this.trpcService.handle( + this.trpcService.trpc.mealPlans.updateMealPlanItem.mutate({ + id: this.mealItem.id, + title: item.title, + recipeId: item.recipeId, + scheduledDate: item.scheduledDate, + meal: item.meal, + }), + ); loading.dismiss(); - if (!response.success) return; + if (!result) return; this.close({ refresh: true, @@ -102,7 +102,7 @@ export class MealPlanItemDetailsModalPage { inputType: this.mealItem.recipe ? "recipe" : "manualEntry", title: this.mealItem.title, recipe: this.mealItem.recipe, - scheduled: this.mealItem.scheduled, + scheduledDate: this.mealItem.scheduledDate, meal: this.mealItem.meal, }, }); @@ -114,19 +114,18 @@ export class MealPlanItemDetailsModalPage { const loading = this.loadingService.start(); - const response = await this.mealPlanService.addItems(this.mealPlanId, { - items: [ - { - title: item.title, - recipeId: item.recipeId, - scheduled: item.scheduled, - meal: item.meal, - }, - ], - }); + const result = await this.trpcService.handle( + this.trpcService.trpc.mealPlans.createMealPlanItem.mutate({ + mealPlanId: this.mealPlanId, + title: item.title, + recipeId: item.recipeId, + scheduledDate: item.scheduledDate, + meal: item.meal, + }), + ); loading.dismiss(); - if (!response.success) return; + if (!result) return; this.close({ refresh: true, @@ -167,11 +166,13 @@ export class MealPlanItemDetailsModalPage { async _delete() { const loading = this.loadingService.start(); - const response = await this.mealPlanService.deleteItems(this.mealPlanId, { - itemIds: this.mealItem.id, - }); + const result = await this.trpcService.handle( + this.trpcService.trpc.mealPlans.deleteMealPlanItem.mutate({ + id: this.mealItem.id, + }), + ); loading.dismiss(); - if (!response.success) return; + if (!result) return; this.close({ refresh: true, @@ -205,7 +206,7 @@ export class MealPlanItemDetailsModalPage { this.cookingToolbarService.pinRecipe({ id: this.mealItem.recipe.id, title: this.mealItem.recipe.title, - imageUrl: this.mealItem.recipe.images[0]?.location, + imageUrl: this.mealItem.recipe.recipeImages.at(0)?.image.location, }); } diff --git a/packages/frontend/src/app/pages/meal-plan-components/meal-plan-popover/meal-plan-popover.page.ts b/packages/frontend/src/app/pages/meal-plan-components/meal-plan-popover/meal-plan-popover.page.ts index fbe0ff16f..6a1c2e721 100644 --- a/packages/frontend/src/app/pages/meal-plan-components/meal-plan-popover/meal-plan-popover.page.ts +++ b/packages/frontend/src/app/pages/meal-plan-components/meal-plan-popover/meal-plan-popover.page.ts @@ -14,6 +14,7 @@ import { UtilService, RouteMap } from "~/services/util.service"; import { PreferencesService } from "~/services/preferences.service"; import { MealPlanPreferenceKey } from "@recipesage/util/shared"; import { ShareMealPlanModalPage } from "../share-meal-plan-modal/share-meal-plan-modal.page"; +import { TRPCService } from "../../../services/trpc.service"; @Component({ selector: "page-meal-plan-popover", @@ -28,16 +29,14 @@ export class MealPlanPopoverPage { mealPlan: any; // From nav params constructor( - public popoverCtrl: PopoverController, - public modalCtrl: ModalController, - public translate: TranslateService, - public navCtrl: NavController, - public utilService: UtilService, - public preferencesService: PreferencesService, - public loadingService: LoadingService, - public mealPlanService: MealPlanService, - public toastCtrl: ToastController, - public alertCtrl: AlertController, + private popoverCtrl: PopoverController, + private modalCtrl: ModalController, + private translate: TranslateService, + private navCtrl: NavController, + private preferencesService: PreferencesService, + private loadingService: LoadingService, + private trpcService: TRPCService, + private alertCtrl: AlertController, ) {} savePreferences() { @@ -128,9 +127,13 @@ export class MealPlanPopoverPage { async _deleteMealPlan() { const loading = this.loadingService.start(); - const response = await this.mealPlanService.delete(this.mealPlanId); + const result = await this.trpcService.handle( + this.trpcService.trpc.mealPlans.deleteMealPlan.mutate({ + id: this.mealPlanId, + }), + ); loading.dismiss(); - if (!response.success) return; + if (!result) return; this.popoverCtrl.dismiss(); this.navCtrl.navigateBack(RouteMap.MealPlansPage.getPath()); diff --git a/packages/frontend/src/app/pages/meal-plan-components/meal-plan/meal-plan.page.html b/packages/frontend/src/app/pages/meal-plan-components/meal-plan/meal-plan.page.html index b3ba650d9..cffdac867 100644 --- a/packages/frontend/src/app/pages/meal-plan-components/meal-plan/meal-plan.page.html +++ b/packages/frontend/src/app/pages/meal-plan-components/meal-plan/meal-plan.page.html @@ -5,7 +5,7 @@ - {{ 'pages.mealPlan.title' | translate:{name:mealPlan.title} }} + {{ 'pages.mealPlan.title' | translate:{name:mealPlan?.title} }} @@ -18,9 +18,9 @@
-
+
600 ? "full" : "split"; dayCopyInProgress = false; dayMoveInProgress = false; - selectedDaysInProgress?: number[]; + selectedDaysInProgress?: string[]; mealPlanId: string; // From nav params - mealPlan: any = { items: [], collaborators: [] }; + mealPlan?: MealPlanSummary; + mealPlanItems?: MealPlanItemSummary[]; mealsByDate: { [year: number]: { [month: number]: { [day: number]: { - items: MealPlanItem[]; + items: MealPlanItemSummary[]; }; }; }; } = {}; - itemsByRecipeId: { [key: string]: MealPlanItem } = {}; + itemsByRecipeId: { [key: string]: MealPlanItemSummary } = {}; recipeIds: string[] = []; preferences = this.preferencesService.preferences; preferenceKeys = MealPlanPreferenceKey; - selectedDays: number[] = []; + selectedDays: string[] = []; @ViewChild(MealCalendarComponent, { static: true }) mealPlanCalendar?: MealCalendarComponent; @@ -71,7 +69,7 @@ export class MealPlanPage { public translate: TranslateService, public navCtrl: NavController, public loadingService: LoadingService, - public mealPlanService: MealPlanService, + public trpcService: TRPCService, public shoppingListService: ShoppingListService, public websocketService: WebsocketService, public utilService: UtilService, @@ -122,25 +120,40 @@ export class MealPlanPage { } async loadMealPlan() { - const response = await this.mealPlanService.fetchById(this.mealPlanId); - if (!response.success) return; - this.mealPlan = response.data; + const mealPlan = await this.trpcService.handle( + this.trpcService.trpc.mealPlans.getMealPlan.query({ + id: this.mealPlanId, + }), + ); + if (!mealPlan) return; + this.mealPlan = mealPlan; + + const mealPlanItems = await this.trpcService.handle( + this.trpcService.trpc.mealPlans.getMealPlanItems.query({ + mealPlanId: this.mealPlanId, + }), + ); + if (!mealPlanItems) return; + this.mealPlanItems = mealPlanItems; } async _addItem(item: { title: string; recipeId?: string; meal: string; - scheduled: string; + scheduledDate: string; }) { const loading = this.loadingService.start(); - await this.mealPlanService.addItem(this.mealPlanId, { - title: item.title, - recipeId: item.recipeId || null, - meal: item.meal, - scheduled: item.scheduled, - }); + await this.trpcService.handle( + this.trpcService.trpc.mealPlans.createMealPlanItem.mutate({ + mealPlanId: this.mealPlanId, + title: item.title, + recipeId: item.recipeId || null, + meal: item.meal as any, // TODO: Refine this type so that it aligns with Zod + scheduledDate: item.scheduledDate, + }), + ); await this.loadMealPlan(); @@ -151,7 +164,7 @@ export class MealPlanPage { const modal = await this.modalCtrl.create({ component: NewMealPlanItemModalPage, componentProps: { - scheduled: new Date(this.selectedDays[0]), + scheduledDate: this.selectedDays[0], }, }); modal.present(); @@ -183,7 +196,7 @@ export class MealPlanPage { if (data?.bulkAddToShoppingList) this.bulkAddToShoppingList(); } - async itemClicked(mealItem: MealPlanItem) { + async itemClicked(mealItem: MealPlanItemSummary) { const modal = await this.modalCtrl.create({ component: MealPlanItemDetailsModalPage, componentProps: { @@ -197,8 +210,14 @@ export class MealPlanPage { if (data?.refresh) this.loadWithProgress(); } - async itemMoved({ day, mealItem }: { day: number; mealItem: MealPlanItem }) { - console.log(day, mealItem); + async itemMoved({ + dateStamp, + mealItem, + }: { + dateStamp: string; + mealItem: MealPlanItemSummary; + }) { + console.log(dateStamp, mealItem); const modal = await this.modalCtrl.create({ component: NewMealPlanItemModalPage, componentProps: { @@ -206,7 +225,7 @@ export class MealPlanPage { inputType: mealItem.recipe ? "recipe" : "manualEntry", title: mealItem.title, recipe: mealItem.recipe, - scheduled: day, + scheduledDate: dateStamp, meal: mealItem.meal, }, }); @@ -217,23 +236,21 @@ export class MealPlanPage { const item = data.item; const loading = this.loadingService.start(); - await this.mealPlanService.updateItems(this.mealPlanId, { - items: [ - { - id: mealItem.id, - title: item.title, - recipeId: item.recipeId, - scheduled: item.scheduled, - meal: item.meal, - }, - ], - }); + await this.trpcService.handle( + this.trpcService.trpc.mealPlans.updateMealPlanItem.mutate({ + id: mealItem.id, + title: item.title, + recipeId: item.recipeId, + scheduledDate: item.scheduledDate, + meal: item.meal, + }), + ); loading.dismiss(); this.loadWithProgress(); } - getItemsOnDay(unix: number) { - const day = dayjs(unix); + getItemsOnDay(dateStamp: string) { + const day = dayjs(dateStamp); return ( this.mealsByDate?.[day.year()]?.[day.month()]?.[day.date()]?.items || [] ); @@ -241,7 +258,7 @@ export class MealPlanPage { getSelectedMealItemCount(): number { return this.selectedDays - .map((unix) => this.getItemsOnDay(unix).length) + .map((dateStamp) => this.getItemsOnDay(dateStamp).length) .reduce((acc, el) => acc + el, 0); } @@ -445,12 +462,12 @@ export class MealPlanPage { modal.present(); } - async dayClicked(day: Date | string | number) { + async dayClicked(dateStamp: string) { if (this.dayMoveInProgress || this.dayCopyInProgress) { const selectedDayList = (this.selectedDaysInProgress || []) .map((selectedDay) => dayjs(selectedDay).format("MMM D")) .join(", "); - const destDay = dayjs(day).format("MMM D"); + const destDay = dayjs(dateStamp).format("MMM D"); if (this.dayCopyInProgress) { const header = await this.translate @@ -492,7 +509,7 @@ export class MealPlanPage { text: okay, handler: async () => { this.dayCopyInProgress = false; - this._copySelectedTo(day); + this._copySelectedTo(dateStamp); }, }, ], @@ -540,7 +557,7 @@ export class MealPlanPage { text: okay, handler: async () => { this.dayMoveInProgress = false; - this._moveSelectedTo(day); + this._moveSelectedTo(dateStamp); }, }, ], @@ -550,59 +567,69 @@ export class MealPlanPage { } } - async _moveSelectedTo(day: Date | string | number) { + async _moveSelectedTo(dateStamp: string) { if (!this.selectedDaysInProgress) throw new Error("Move initiated with no selected days"); - const dayDiff = dayjs(day).diff(this.selectedDaysInProgress[0], "day"); + const dayDiff = dayjs(dateStamp).diff( + this.selectedDaysInProgress[0], + "day", + ); const updatedItems = this.selectedDaysInProgress .map((selectedDay) => this.getItemsOnDay(selectedDay).map((item) => ({ id: item.id, title: item.title, - recipeId: item.recipeId || item.recipe?.id, - scheduled: dayjs(item.scheduled) + recipeId: item.recipeId, + scheduledDate: dayjs(item.scheduledDate) .add(dayDiff, "day") - .toDate() - .toISOString(), - meal: item.meal, + .format("YYYY-MM-DD"), + meal: item.meal as any, // TODO: Refine this type so that it aligns with Zod })), ) .flat(); const loading = this.loadingService.start(); - await this.mealPlanService.updateItems(this.mealPlanId, { - items: updatedItems, - }); + await this.trpcService.handle( + this.trpcService.trpc.mealPlans.updateMealPlanItems.mutate({ + mealPlanId: this.mealPlanId, + items: updatedItems, + }), + ); loading.dismiss(); this.loadWithProgress(); } - async _copySelectedTo(day: Date | string | number) { + async _copySelectedTo(dateStamp: string) { if (!this.selectedDaysInProgress) throw new Error("Move initiated with no selected days"); - const dayDiff = dayjs(day).diff(this.selectedDaysInProgress[0], "day"); + const dayDiff = dayjs(dateStamp).diff( + this.selectedDaysInProgress[0], + "day", + ); const newItems = this.selectedDaysInProgress .map((selectedDay) => this.getItemsOnDay(selectedDay).map((item) => ({ title: item.title, - recipeId: item.recipeId || item.recipe?.id, - scheduled: dayjs(item.scheduled) + recipeId: item.recipeId, + scheduledDate: dayjs(item.scheduled) .add(dayDiff, "day") - .toDate() - .toISOString(), - meal: item.meal, + .format("YYYY-MM-DD"), + meal: item.meal as any, // TODO: Refine this type so that it aligns with Zod })), ) .flat(); const loading = this.loadingService.start(); - await this.mealPlanService.addItems(this.mealPlanId, { - items: newItems, - }); + await this.trpcService.handle( + this.trpcService.trpc.mealPlans.createMealPlanItems.mutate({ + mealPlanId: this.mealPlanId, + items: newItems, + }), + ); loading.dismiss(); this.loadWithProgress(); } @@ -613,9 +640,12 @@ export class MealPlanPage { .flat(); const loading = this.loadingService.start(); - await this.mealPlanService.deleteItems(this.mealPlanId, { - itemIds: itemIds.join(","), - }); + await this.trpcService.handle( + this.trpcService.trpc.mealPlans.deleteMealPlanItems.mutate({ + mealPlanId: this.mealPlanId, + ids: itemIds, + }), + ); loading.dismiss(); this.loadWithProgress(); } @@ -624,7 +654,7 @@ export class MealPlanPage { this.mealsByDate = mealsByDate; } - setSelectedDays(selectedDays: number[]) { + setSelectedDays(selectedDays: string[]) { this.selectedDays = selectedDays; } } diff --git a/packages/frontend/src/app/pages/meal-plan-components/meal-plans/meal-plans.page.html b/packages/frontend/src/app/pages/meal-plan-components/meal-plans/meal-plans.page.html index cb405df22..14eb6a883 100644 --- a/packages/frontend/src/app/pages/meal-plan-components/meal-plans/meal-plans.page.html +++ b/packages/frontend/src/app/pages/meal-plan-components/meal-plans/meal-plans.page.html @@ -26,33 +26,33 @@

{{ mealPlan.title }}

{{ 'pages.mealPlans.metadata.createdOn' | translate:{date:formatItemCreationDate(mealPlan.createdAt)} }}

-

+

{{ 'pages.mealPlans.metadata.collaborators' | translate }} - - - {{ user.name || user.email }}, + + + {{ collaboratorUser.user.name || collaboratorUser.user.email + }}, - - {{ mealPlan.owner.name || mealPlan.owner.email }} + + {{ mealPlan.user.name || mealPlan.user.email }}

- + {{ 'pages.mealPlans.metadata.createdBy' | - translate:{name:(mealPlan.owner.name || mealPlan.owner.email)} }} + translate:{name:(mealPlan.user.name || mealPlan.user.email)} }} - + {{ 'pages.mealPlans.metadata.youOwn' | translate }}

- {{ mealPlan.itemCount }} + {{ mealPlan._count.items }} - + { + Promise.all([this.loadPlans(), this.loadMe()]).finally(() => { loading.dismiss(); - this.initialLoadComplete = true; }); } @@ -71,11 +70,22 @@ export class MealPlansPage { ); } + async loadMe() { + const me = await this.trpcService.handle( + this.trpcService.trpc.users.getMe.query(), + ); + if (!me) return; + + this.me = me; + } + async loadPlans() { - const response = await this.mealPlanService.fetch(); - if (!response.success) return; + const mealPlans = await this.trpcService.handle( + this.trpcService.trpc.mealPlans.getMealPlans.query(), + ); + if (!mealPlans) return; - this.mealPlans = response.data.sort((a, b) => { + this.mealPlans = mealPlans.sort((a, b) => { return a.title.localeCompare(b.title); }); } @@ -94,7 +104,7 @@ export class MealPlansPage { this.navCtrl.navigateForward(RouteMap.MealPlanPage.getPath(mealPlanId)); } - formatItemCreationDate(plainTextDate: string) { - return this.utilService.formatDate(plainTextDate, { now: true }); + formatItemCreationDate(date: string | Date) { + return this.utilService.formatDate(date, { now: true }); } } diff --git a/packages/frontend/src/app/pages/meal-plan-components/new-meal-plan-item-modal/new-meal-plan-item-modal.page.html b/packages/frontend/src/app/pages/meal-plan-components/new-meal-plan-item-modal/new-meal-plan-item-modal.page.html index 888f56feb..0c8292ec9 100644 --- a/packages/frontend/src/app/pages/meal-plan-components/new-meal-plan-item-modal/new-meal-plan-item-modal.page.html +++ b/packages/frontend/src/app/pages/meal-plan-components/new-meal-plan-item-modal/new-meal-plan-item-modal.page.html @@ -57,7 +57,7 @@ class="rs-date" type="date" autofill="true" - value="{{ sanitizedScheduled }}" + [value]="scheduledDate" (change)="scheduledDateChange($event)" /> diff --git a/packages/frontend/src/app/pages/meal-plan-components/new-meal-plan-item-modal/new-meal-plan-item-modal.page.ts b/packages/frontend/src/app/pages/meal-plan-components/new-meal-plan-item-modal/new-meal-plan-item-modal.page.ts index d0ca585a2..21c5d538b 100644 --- a/packages/frontend/src/app/pages/meal-plan-components/new-meal-plan-item-modal/new-meal-plan-item-modal.page.ts +++ b/packages/frontend/src/app/pages/meal-plan-components/new-meal-plan-item-modal/new-meal-plan-item-modal.page.ts @@ -1,4 +1,5 @@ import { Input, Component } from "@angular/core"; +import dayjs from "dayjs"; import { NavController, ModalController, @@ -19,9 +20,7 @@ export class NewMealPlanItemModalPage { @Input() recipe?: Recipe; @Input() title: string = ""; @Input() meal?: string; - @Input() scheduled = new Date(); - - sanitizedScheduled: string = ""; + @Input() scheduledDate = dayjs().format("YYYY-MM-DD"); constructor( public navCtrl: NavController, @@ -30,28 +29,10 @@ export class NewMealPlanItemModalPage { public loadingService: LoadingService, public utilService: UtilService, public toastCtrl: ToastController, - ) { - setTimeout(() => { - this.setSanitizedScheduled(); - }); - } - - setSanitizedScheduled() { - const scheduled = new Date(this.scheduled); - const year = scheduled.getFullYear(); - const month = (scheduled.getMonth() + 1).toString().padStart(2, "0"); - const date = scheduled.getDate().toString().padStart(2, "0"); - - this.sanitizedScheduled = `${year}-${month}-${date}`; - } + ) {} scheduledDateChange(event: any) { - const [year, month, date] = event.target.value.split("-"); - const scheduled = new Date(); - scheduled.setDate(date); - scheduled.setMonth(month - 1); - scheduled.setFullYear(year); - this.scheduled = scheduled; + this.scheduledDate = dayjs(event.target.value).format("YYYY-MM-DD"); } isFormValid() { @@ -69,7 +50,7 @@ export class NewMealPlanItemModalPage { } save() { - if (!this.meal || !this.scheduled) return; + if (!this.meal || !this.scheduledDate) return; const item = { title: @@ -77,9 +58,9 @@ export class NewMealPlanItemModalPage { ? this.recipe.title : this.title, recipeId: - this.inputType === "recipe" && this.recipe ? this.recipe.id : undefined, + this.inputType === "recipe" && this.recipe ? this.recipe.id : null, meal: this.meal, - scheduled: this.scheduled, + scheduledDate: this.scheduledDate, }; this.modalCtrl.dismiss({ diff --git a/packages/frontend/src/app/pages/meal-plan-components/new-meal-plan-modal/new-meal-plan-modal.page.ts b/packages/frontend/src/app/pages/meal-plan-components/new-meal-plan-modal/new-meal-plan-modal.page.ts index 3d7321025..751b2fe43 100644 --- a/packages/frontend/src/app/pages/meal-plan-components/new-meal-plan-modal/new-meal-plan-modal.page.ts +++ b/packages/frontend/src/app/pages/meal-plan-components/new-meal-plan-modal/new-meal-plan-modal.page.ts @@ -1,14 +1,9 @@ import { Component } from "@angular/core"; -import { - NavController, - ModalController, - ToastController, -} from "@ionic/angular"; +import { NavController, ModalController } from "@ionic/angular"; import { LoadingService } from "~/services/loading.service"; -import { MessagingService } from "~/services/messaging.service"; -import { MealPlanService } from "~/services/meal-plan.service"; -import { UtilService, RouteMap, AuthType } from "~/services/util.service"; +import { RouteMap } from "~/services/util.service"; +import { TRPCService } from "../../../services/trpc.service"; @Component({ selector: "page-new-meal-plan-modal", @@ -21,31 +16,28 @@ export class NewMealPlanModalPage { selectedCollaboratorIds: any = []; constructor( - public navCtrl: NavController, - public modalCtrl: ModalController, - public loadingService: LoadingService, - public mealPlanService: MealPlanService, - public messagingService: MessagingService, - public utilService: UtilService, - public toastCtrl: ToastController, + private navCtrl: NavController, + private modalCtrl: ModalController, + private loadingService: LoadingService, + private trpcService: TRPCService, ) {} async save() { const loading = this.loadingService.start(); - const response = await this.mealPlanService.create({ - title: this.mealPlanTitle, - collaborators: this.selectedCollaboratorIds, - }); + const result = await this.trpcService.handle( + this.trpcService.trpc.mealPlans.createMealPlan.mutate({ + title: this.mealPlanTitle, + collaboratorUserIds: this.selectedCollaboratorIds, + }), + ); loading.dismiss(); - if (!response.success) return; + if (!result) return; this.modalCtrl.dismiss({ success: true, }); - this.navCtrl.navigateForward( - RouteMap.MealPlanPage.getPath(response.data.id), - ); + this.navCtrl.navigateForward(RouteMap.MealPlanPage.getPath(result.id)); } cancel() { diff --git a/packages/frontend/src/app/pages/recipe-components/add-recipe-to-meal-plan-modal/add-recipe-to-meal-plan-modal.page.html b/packages/frontend/src/app/pages/recipe-components/add-recipe-to-meal-plan-modal/add-recipe-to-meal-plan-modal.page.html index e79d667d1..91e37260b 100644 --- a/packages/frontend/src/app/pages/recipe-components/add-recipe-to-meal-plan-modal/add-recipe-to-meal-plan-modal.page.html +++ b/packages/frontend/src/app/pages/recipe-components/add-recipe-to-meal-plan-modal/add-recipe-to-meal-plan-modal.page.html @@ -13,7 +13,7 @@ -

+

{{ 'pages.addRecipeToMealPlanModal.instructions1' | translate }}

@@ -47,9 +47,9 @@

-
+
@@ -64,11 +64,11 @@ {{ 'pages.addRecipeToMealPlanModal.add' | translate }} - {{ 'pages.addRecipeToMealPlanModal.selectMealPlan' | translate }} - {{ 'pages.addRecipeToMealPlanModal.selectMeal' | translate }} diff --git a/packages/frontend/src/app/pages/recipe-components/add-recipe-to-meal-plan-modal/add-recipe-to-meal-plan-modal.page.ts b/packages/frontend/src/app/pages/recipe-components/add-recipe-to-meal-plan-modal/add-recipe-to-meal-plan-modal.page.ts index 6f0e777b4..b844814f1 100644 --- a/packages/frontend/src/app/pages/recipe-components/add-recipe-to-meal-plan-modal/add-recipe-to-meal-plan-modal.page.ts +++ b/packages/frontend/src/app/pages/recipe-components/add-recipe-to-meal-plan-modal/add-recipe-to-meal-plan-modal.page.ts @@ -1,22 +1,12 @@ import { Component, Input } from "@angular/core"; -import { - NavController, - ToastController, - ModalController, - AlertController, -} from "@ionic/angular"; +import { ToastController, ModalController } from "@ionic/angular"; import { LoadingService } from "~/services/loading.service"; -import { RecipeService } from "~/services/recipe.service"; -import { UtilService } from "~/services/util.service"; -import { - MealPlan, - MealPlans, - MealPlanService, -} from "~/services/meal-plan.service"; import { NewMealPlanModalPage } from "~/pages/meal-plan-components/new-meal-plan-modal/new-meal-plan-modal.page"; import { TranslateService } from "@ngx-translate/core"; +import { TRPCService } from "../../../services/trpc.service"; +import { MealPlanItemSummary, MealPlanSummary } from "@recipesage/prisma"; @Component({ selector: "page-add-recipe-to-meal-plan-modal", @@ -26,26 +16,22 @@ import { TranslateService } from "@ngx-translate/core"; export class AddRecipeToMealPlanModalPage { @Input() recipe: any; - mealPlans?: MealPlans; + mealPlans?: MealPlanSummary[]; - selectedMealPlan?: MealPlans[0]; - destinationMealPlan?: MealPlan; + selectedMealPlan?: MealPlanSummary; + selectedMealPlanItems?: MealPlanItemSummary[]; meal?: string; @Input() reference?: string; - selectedDays: number[] = []; + selectedDays: string[] = []; constructor( - public navCtrl: NavController, - public translate: TranslateService, - public mealPlanService: MealPlanService, - public recipeService: RecipeService, - public loadingService: LoadingService, - public utilService: UtilService, - public toastCtrl: ToastController, - public alertCtrl: AlertController, - public modalCtrl: ModalController, + private translate: TranslateService, + private trpcService: TRPCService, + private loadingService: LoadingService, + private toastCtrl: ToastController, + private modalCtrl: ModalController, ) {} ionViewWillEnter() { @@ -83,46 +69,53 @@ export class AddRecipeToMealPlanModalPage { } async loadMealPlans() { - const response = await this.mealPlanService.fetch(); - if (!response.success) return; + const mealPlans = await this.trpcService.handle( + this.trpcService.trpc.mealPlans.getMealPlans.query(), + ); + if (!mealPlans) return; - this.mealPlans = response.data; + this.mealPlans = mealPlans; this.selectLastUsedMealPlan(); } async loadMealPlan(id: string) { - const response = await this.mealPlanService.fetchById(id); - if (!response.success) return; + const mealPlanItems = await this.trpcService.handle( + this.trpcService.trpc.mealPlans.getMealPlanItems.query({ + mealPlanId: id, + }), + ); - this.destinationMealPlan = response.data; + if (!mealPlanItems) return; + + this.selectedMealPlanItems = mealPlanItems; } isFormValid() { - if (!this.destinationMealPlan) return false; + if (!this.selectedMealPlan || !this.selectedDays[0]) return false; return this.meal && this.meal.length > 0; } async save() { - if (!this.destinationMealPlan || !this.meal) return; + if (!this.selectedMealPlan || !this.selectedDays[0] || !this.meal) return; const loading = this.loadingService.start(); this.saveLastUsedMealPlan(); - const response = await this.mealPlanService.addItem( - this.destinationMealPlan.id, - { + const result = await this.trpcService.handle( + this.trpcService.trpc.mealPlans.createMealPlanItem.mutate({ + mealPlanId: this.selectedMealPlan.id, title: this.recipe.title, recipeId: this.recipe.id, - meal: this.meal, - scheduled: new Date(this.selectedDays[0]).toISOString(), - }, + meal: this.meal as any, // TODO: Refine this type so that it aligns with Zod + scheduledDate: this.selectedDays[0], + }), ); loading.dismiss(); - if (response.success) this.modalCtrl.dismiss(); + if (result) this.modalCtrl.dismiss(); } async createMealPlan() { diff --git a/packages/frontend/src/assets/i18n/zh-cn.json b/packages/frontend/src/assets/i18n/zh-cn.json index 51cef267e..79eacaefb 100644 --- a/packages/frontend/src/assets/i18n/zh-cn.json +++ b/packages/frontend/src/assets/i18n/zh-cn.json @@ -1003,5 +1003,10 @@ "generic.okay": "OK", "generic.ok": "Ok", "generic.ignore": "忽略", - "generic.options": "选项" + "generic.options": "选项", + "pages.settings.resetPrefs.message": "重置您的偏好选项可以让所有的设置恢复初始设置.包括在主页的,膳食计划页面偏好,购物清单页面.请注意:这个设置只会在本设备生效.", + "pages.shoppingListIgnoreModal.title": "购物清单例外项目列表", + "pages.profile.alreadyRequested": "看上去您有一个好友关系在进行中.请再次尝试.", + "components.selectIngredients.scale": "{{scale}}x 倍大小 (点击修改)", + "components.selectUser.placeholder": "根据email或者handle搜索" } diff --git a/packages/prisma/src/prisma/migrations/20240313011149_update_tables_nonnullable/migration.sql b/packages/prisma/src/prisma/migrations/20240313011149_update_tables_nonnullable/migration.sql new file mode 100644 index 000000000..91471abc4 --- /dev/null +++ b/packages/prisma/src/prisma/migrations/20240313011149_update_tables_nonnullable/migration.sql @@ -0,0 +1,29 @@ +-- AlterTable +ALTER TABLE "FCMTokens" ALTER COLUMN "token" SET NOT NULL; + +-- AlterTable +ALTER TABLE "Images" ALTER COLUMN "location" SET NOT NULL, +ALTER COLUMN "key" SET NOT NULL, +ALTER COLUMN "json" SET NOT NULL; + +-- AlterTable +ALTER TABLE "MealPlanItems" ALTER COLUMN "title" SET NOT NULL, +ALTER COLUMN "meal" SET NOT NULL; + +-- AlterTable +ALTER TABLE "MealPlans" ALTER COLUMN "title" SET NOT NULL; + +-- AlterTable +ALTER TABLE "Sessions" ALTER COLUMN "type" SET NOT NULL, +ALTER COLUMN "token" SET NOT NULL, +ALTER COLUMN "expires" SET NOT NULL; + +-- AlterTable +ALTER TABLE "ShoppingLists" ALTER COLUMN "title" SET NOT NULL; + +-- AlterTable +ALTER TABLE "UserSubscriptions" ALTER COLUMN "name" SET NOT NULL; + +-- AlterTable +ALTER TABLE "Users" ALTER COLUMN "name" SET NOT NULL, +ALTER COLUMN "email" SET NOT NULL; diff --git a/packages/prisma/src/prisma/migrations/20240313051131_add_mealplanitem_scheduleddate/migration.sql b/packages/prisma/src/prisma/migrations/20240313051131_add_mealplanitem_scheduleddate/migration.sql new file mode 100644 index 000000000..ebbde3cd8 --- /dev/null +++ b/packages/prisma/src/prisma/migrations/20240313051131_add_mealplanitem_scheduleddate/migration.sql @@ -0,0 +1,6 @@ +-- AlterTable +ALTER TABLE "MealPlanItems" ADD COLUMN "scheduledDate" DATE; + +UPDATE "MealPlanItems" SET "scheduledDate" = "scheduled"; + +ALTER TABLE "MealPlanItems" ALTER COLUMN "scheduledDate" SET NOT NULL; diff --git a/packages/prisma/src/prisma/schema.prisma b/packages/prisma/src/prisma/schema.prisma index 0c6ca405c..ee2248851 100644 --- a/packages/prisma/src/prisma/schema.prisma +++ b/packages/prisma/src/prisma/schema.prisma @@ -10,7 +10,7 @@ datasource db { model FCMToken { id String @id @default(uuid()) @db.Uuid userId String @db.Uuid - token String? + token String createdAt DateTime @default(now()) @db.Timestamptz(6) updatedAt DateTime @updatedAt @db.Timestamptz(6) users User @relation(fields: [userId], references: [id], onDelete: Cascade) @@ -34,9 +34,9 @@ model Friendship { model Image { id String @id @default(uuid()) @db.Uuid userId String? @db.Uuid - location String? @db.VarChar(255) - key String? @db.VarChar(255) - json Json? + location String @db.VarChar(255) + key String @db.VarChar(255) + json Json createdAt DateTime @default(now()) @db.Timestamptz(6) updatedAt DateTime @updatedAt @db.Timestamptz(6) user User? @relation(fields: [userId], references: [id], onDelete: SetNull) @@ -85,9 +85,10 @@ model MealPlanItem { userId String @db.Uuid mealPlanId String @db.Uuid recipeId String? @db.Uuid - title String? + title String + scheduledDate DateTime @db.Date scheduled DateTime? @db.Timestamptz(6) - meal String? @db.VarChar(255) + meal String @db.VarChar(255) createdAt DateTime @default(now()) @db.Timestamptz(6) updatedAt DateTime @updatedAt @db.Timestamptz(6) mealPlan MealPlan @relation(fields: [mealPlanId], references: [id], onDelete: Cascade) @@ -116,7 +117,7 @@ model MealPlanCollaborator { model MealPlan { id String @id @default(uuid()) @db.Uuid userId String @db.Uuid - title String? + title String createdAt DateTime @default(now()) @db.Timestamptz(6) updatedAt DateTime @updatedAt @db.Timestamptz(6) items MealPlanItem[] @@ -247,14 +248,14 @@ model Recipe { } model Session { - id String @id @default(uuid()) @db.Uuid - userId String @db.Uuid - type String? @db.VarChar(255) - token String? @db.VarChar(255) - expires DateTime? @db.Timestamptz(6) - createdAt DateTime @default(now()) @db.Timestamptz(6) - updatedAt DateTime @updatedAt @db.Timestamptz(6) - user User @relation(fields: [userId], references: [id], onDelete: Cascade) + id String @id @default(uuid()) @db.Uuid + userId String @db.Uuid + type String @db.VarChar(255) + token String @db.VarChar(255) + expires DateTime @db.Timestamptz(6) + createdAt DateTime @default(now()) @db.Timestamptz(6) + updatedAt DateTime @updatedAt @db.Timestamptz(6) + user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@map("Sessions") } @@ -295,7 +296,7 @@ model ShoppingListCollaborator { model ShoppingList { id String @id @default(uuid()) @db.Uuid userId String @db.Uuid - title String? + title String createdAt DateTime @default(now()) @db.Timestamptz(6) updatedAt DateTime @updatedAt @db.Timestamptz(6) items ShoppingListItem[] @@ -324,7 +325,7 @@ model StripePayment { model UserSubscription { id String @id @default(uuid()) @db.Uuid userId String @db.Uuid - name String? @db.VarChar(255) + name String @db.VarChar(255) expires DateTime? @db.Timestamptz(6) createdAt DateTime @default(now()) @db.Timestamptz(6) updatedAt DateTime @updatedAt @db.Timestamptz(6) @@ -348,8 +349,8 @@ model UserProfileImage { model User { id String @id @default(uuid()) @db.Uuid - name String? - email String? @unique(map: "Users_email_uk") + name String + email String @unique(map: "Users_email_uk") passwordHash String? passwordSalt String? passwordVersion Int? diff --git a/packages/prisma/src/types/index.ts b/packages/prisma/src/types/index.ts index 861afa392..1a5adc2c2 100644 --- a/packages/prisma/src/types/index.ts +++ b/packages/prisma/src/types/index.ts @@ -1,6 +1,8 @@ export * from "./assistantMessageSummary"; export * from "./labelGroupSummary"; export * from "./labelSummary"; +export * from "./mealPlanSummary"; +export * from "./mealPlanItemSummary"; export * from "./recipeSummary"; export * from "./recipeSummaryLite"; export * from "./userPublic"; diff --git a/packages/prisma/src/types/mealPlanItemSummary.ts b/packages/prisma/src/types/mealPlanItemSummary.ts new file mode 100644 index 000000000..ab339e3a8 --- /dev/null +++ b/packages/prisma/src/types/mealPlanItemSummary.ts @@ -0,0 +1,63 @@ +import { Prisma } from "@prisma/client"; +import { userPublic } from "./userPublic"; + +/** + * Provides fields necessary for displaying a summary about a meal plan item + **/ +export const mealPlanItemSummary = Prisma.validator()({ + select: { + id: true, + title: true, + scheduled: true, + scheduledDate: true, + meal: true, + createdAt: true, + updatedAt: true, + user: userPublic, + shoppingListItems: { + select: { + id: true, + title: true, + shoppingListId: true, + shoppingList: { + select: { + id: true, + title: true, + }, + }, + }, + }, + recipeId: true, + recipe: { + select: { + id: true, + title: true, + ingredients: true, + recipeImages: { + select: { + image: { + select: { + id: true, + location: true, + }, + }, + }, + }, + }, + }, + }, +}); + +type InternalMealPlanItemSummary = Prisma.MealPlanItemGetPayload< + typeof mealPlanItemSummary +>; + +/** + * Provides fields necessary for displaying a summary about a meal plan item + **/ +export type MealPlanItemSummary = Omit< + InternalMealPlanItemSummary, + "scheduledDate" +> & { + scheduledDate: string; +}; diff --git a/packages/prisma/src/types/mealPlanSummary.ts b/packages/prisma/src/types/mealPlanSummary.ts new file mode 100644 index 000000000..085b28bca --- /dev/null +++ b/packages/prisma/src/types/mealPlanSummary.ts @@ -0,0 +1,33 @@ +import { Prisma } from "@prisma/client"; +import { userPublic } from "./userPublic"; + +/** + * Provides fields necessary for displaying a summary about a meal plan, + * not including items + **/ +export const mealPlanSummary = Prisma.validator()({ + select: { + id: true, + userId: true, + user: userPublic, + collaboratorUsers: { + select: { + user: userPublic, + }, + }, + title: true, + createdAt: true, + updatedAt: true, + _count: { + select: { + items: true, + }, + }, + }, +}); + +/** + * Provides fields necessary for displaying a summary about a meal plan, + * not including items + **/ +export type MealPlanSummary = Prisma.MealPlanGetPayload; diff --git a/packages/prisma/src/types/mealPlanSummaryWithItems.ts b/packages/prisma/src/types/mealPlanSummaryWithItems.ts new file mode 100644 index 000000000..7574a312c --- /dev/null +++ b/packages/prisma/src/types/mealPlanSummaryWithItems.ts @@ -0,0 +1,73 @@ +import { Prisma } from "@prisma/client"; +import { userPublic } from "./userPublic"; + +/** + * Provides fields necessary for displaying a summary about a meal plan + **/ +export const mealPlanSummaryWithItems = Prisma.validator()( + { + select: { + id: true, + userId: true, + user: userPublic, + collaboratorUsers: { + select: { + user: userPublic, + }, + }, + title: true, + createdAt: true, + updatedAt: true, + items: { + select: { + id: true, + title: true, + scheduled: true, + scheduledDate: true, + meal: true, + createdAt: true, + updatedAt: true, + user: userPublic, + shoppingListItems: { + select: { + id: true, + title: true, + shoppingListId: true, + shoppingList: { + select: { + id: true, + title: true, + }, + }, + }, + }, + recipe: { + select: { + id: true, + title: true, + ingredients: true, + recipeImages: { + select: { + image: { + select: { + id: true, + location: true, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, +); + +/** + * Provides fields necessary for displaying a summary about a meal plan, + * not including items + **/ +export type MealPlanSummaryWithItems = Prisma.MealPlanGetPayload< + typeof mealPlanSummaryWithItems +>; diff --git a/packages/trpc/src/index.ts b/packages/trpc/src/index.ts index 2e3a47565..0d2f71bcb 100644 --- a/packages/trpc/src/index.ts +++ b/packages/trpc/src/index.ts @@ -32,6 +32,19 @@ import { getUniqueRecipeTitle } from "./procedures/recipes/getUniqueRecipeTitle" import { getRecipeFromPDF } from "./procedures/ml/getRecipeFromPDF"; import { getRecipeFromText } from "./procedures/ml/getRecipeFromText"; import { signInWithGoogle } from "./procedures/users/signInWithGoogle"; +import { getMealPlan } from "./procedures/mealPlans/getMealPlan"; +import { getMealPlanItems } from "./procedures/mealPlans/getMealPlanItems"; +import { getMealPlans } from "./procedures/mealPlans/getMealPlans"; +import { createMealPlan } from "./procedures/mealPlans/createMealPlan"; +import { createMealPlanItem } from "./procedures/mealPlans/createMealPlanItem"; +import { deleteMealPlan } from "./procedures/mealPlans/deleteMealPlan"; +import { deleteMealPlanItem } from "./procedures/mealPlans/deleteMealPlanItem"; +import { detachMealPlan } from "./procedures/mealPlans/detachMealPlan"; +import { updateMealPlan } from "./procedures/mealPlans/updateMealPlan"; +import { updateMealPlanItem } from "./procedures/mealPlans/updateMealPlanItem"; +import { createMealPlanItems } from "./procedures/mealPlans/createMealPlanItems"; +import { deleteMealPlanItems } from "./procedures/mealPlans/deleteMealPlanItems"; +import { updateMealPlanItems } from "./procedures/mealPlans/updateMealPlanItems"; const appRouter = router({ labelGroups: router({ @@ -59,6 +72,21 @@ const appRouter = router({ getRecipesByTitle, getUniqueRecipeTitle, }), + mealPlans: router({ + createMealPlan, + createMealPlanItem, + createMealPlanItems, + deleteMealPlan, + deleteMealPlanItem, + deleteMealPlanItems, + detachMealPlan, + getMealPlan, + getMealPlanItems, + getMealPlans, + updateMealPlan, + updateMealPlanItem, + updateMealPlanItems, + }), assistant: router({ sendAssistantMessage, getAssistantMessages, @@ -74,11 +102,6 @@ const appRouter = router({ getRecipeFromPDF, getRecipeFromText, }), - - // TODO: Legacy compat remove - getRecipes, - searchRecipes, - getSimilarRecipes, }); export const trpcExpressMiddleware = createExpressMiddleware({ diff --git a/packages/trpc/src/procedures/labelGroups/createLabelGroup.ts b/packages/trpc/src/procedures/labelGroups/createLabelGroup.ts index 226fbf3ba..8ebc2fd96 100644 --- a/packages/trpc/src/procedures/labelGroups/createLabelGroup.ts +++ b/packages/trpc/src/procedures/labelGroups/createLabelGroup.ts @@ -1,6 +1,6 @@ import { prisma } from "@recipesage/prisma"; import { publicProcedure } from "../../trpc"; -import { validateSession } from "@recipesage/util/server/general"; +import { validateTrpcSession } from "@recipesage/util/server/general"; import { labelGroupSummary } from "@recipesage/prisma"; import { z } from "zod"; import { TRPCError } from "@trpc/server"; @@ -15,7 +15,7 @@ export const createLabelGroup = publicProcedure ) .mutation(async ({ ctx, input }) => { const session = ctx.session; - validateSession(session); + validateTrpcSession(session); const existingLabelGroup = await prisma.labelGroup.findFirst({ where: { diff --git a/packages/trpc/src/procedures/labelGroups/deleteLabelGroup.ts b/packages/trpc/src/procedures/labelGroups/deleteLabelGroup.ts index d17a9d694..6961c90aa 100644 --- a/packages/trpc/src/procedures/labelGroups/deleteLabelGroup.ts +++ b/packages/trpc/src/procedures/labelGroups/deleteLabelGroup.ts @@ -1,6 +1,6 @@ import { prisma } from "@recipesage/prisma"; import { publicProcedure } from "../../trpc"; -import { validateSession } from "@recipesage/util/server/general"; +import { validateTrpcSession } from "@recipesage/util/server/general"; import { labelGroupSummary } from "@recipesage/prisma"; import { z } from "zod"; import { TRPCError } from "@trpc/server"; @@ -13,7 +13,7 @@ export const deleteLabelGroup = publicProcedure ) .mutation(async ({ ctx, input }) => { const session = ctx.session; - validateSession(session); + validateTrpcSession(session); const labelGroup = await prisma.labelGroup.findUnique({ where: { diff --git a/packages/trpc/src/procedures/labelGroups/getLabelGroups.ts b/packages/trpc/src/procedures/labelGroups/getLabelGroups.ts index aaf7569d2..eaecd7a53 100644 --- a/packages/trpc/src/procedures/labelGroups/getLabelGroups.ts +++ b/packages/trpc/src/procedures/labelGroups/getLabelGroups.ts @@ -1,11 +1,11 @@ import { prisma } from "@recipesage/prisma"; import { publicProcedure } from "../../trpc"; -import { validateSession } from "@recipesage/util/server/general"; +import { validateTrpcSession } from "@recipesage/util/server/general"; import { labelGroupSummary } from "@recipesage/prisma"; export const getLabelGroups = publicProcedure.query(async ({ ctx }) => { const session = ctx.session; - validateSession(session); + validateTrpcSession(session); const labelGroups = await prisma.labelGroup.findMany({ where: { diff --git a/packages/trpc/src/procedures/labelGroups/updateLabelGroup.ts b/packages/trpc/src/procedures/labelGroups/updateLabelGroup.ts index cbe2461d6..383cd1e2d 100644 --- a/packages/trpc/src/procedures/labelGroups/updateLabelGroup.ts +++ b/packages/trpc/src/procedures/labelGroups/updateLabelGroup.ts @@ -1,6 +1,6 @@ import { prisma } from "@recipesage/prisma"; import { publicProcedure } from "../../trpc"; -import { validateSession } from "@recipesage/util/server/general"; +import { validateTrpcSession } from "@recipesage/util/server/general"; import { labelGroupSummary } from "@recipesage/prisma"; import { z } from "zod"; import { TRPCError } from "@trpc/server"; @@ -16,7 +16,7 @@ export const updateLabelGroup = publicProcedure ) .mutation(async ({ ctx, input }) => { const session = ctx.session; - validateSession(session); + validateTrpcSession(session); const existingLabelGroup = await prisma.labelGroup.findFirst({ where: { diff --git a/packages/trpc/src/procedures/labels/createLabel.ts b/packages/trpc/src/procedures/labels/createLabel.ts index f0171a397..dd9f2b90d 100644 --- a/packages/trpc/src/procedures/labels/createLabel.ts +++ b/packages/trpc/src/procedures/labels/createLabel.ts @@ -1,7 +1,7 @@ import { prisma } from "@recipesage/prisma"; import { publicProcedure } from "../../trpc"; import { z } from "zod"; -import { validateSession } from "@recipesage/util/server/general"; +import { validateTrpcSession } from "@recipesage/util/server/general"; import { labelSummary } from "@recipesage/prisma"; import { TRPCError } from "@trpc/server"; import { cleanLabelTitle } from "@recipesage/util/shared"; @@ -15,7 +15,7 @@ export const createLabel = publicProcedure ) .mutation(async ({ ctx, input }) => { const session = ctx.session; - validateSession(session); + validateTrpcSession(session); const title = cleanLabelTitle(input.title); if (!title.length) { diff --git a/packages/trpc/src/procedures/labels/deleteLabel.ts b/packages/trpc/src/procedures/labels/deleteLabel.ts index 5c291c3bd..43be9d654 100644 --- a/packages/trpc/src/procedures/labels/deleteLabel.ts +++ b/packages/trpc/src/procedures/labels/deleteLabel.ts @@ -1,6 +1,6 @@ import { prisma } from "@recipesage/prisma"; import { publicProcedure } from "../../trpc"; -import { validateSession } from "@recipesage/util/server/general"; +import { validateTrpcSession } from "@recipesage/util/server/general"; import { labelSummary } from "@recipesage/prisma"; import { z } from "zod"; import { TRPCError } from "@trpc/server"; @@ -14,7 +14,7 @@ export const deleteLabel = publicProcedure ) .mutation(async ({ ctx, input }) => { const session = ctx.session; - validateSession(session); + validateTrpcSession(session); const label = await prisma.label.findUnique({ where: { diff --git a/packages/trpc/src/procedures/labels/getAllVisibleLabels.ts b/packages/trpc/src/procedures/labels/getAllVisibleLabels.ts index 684349ee3..633c89dc5 100644 --- a/packages/trpc/src/procedures/labels/getAllVisibleLabels.ts +++ b/packages/trpc/src/procedures/labels/getAllVisibleLabels.ts @@ -1,10 +1,10 @@ import { publicProcedure } from "../../trpc"; import { getVisibleLabels } from "@recipesage/util/server/db"; -import { validateSession } from "@recipesage/util/server/general"; +import { validateTrpcSession } from "@recipesage/util/server/general"; export const getAllVisibleLabels = publicProcedure.query(async ({ ctx }) => { const session = ctx.session; - validateSession(session); + validateTrpcSession(session); const visibleLabels = await getVisibleLabels(session.userId, { includeSelf: true, diff --git a/packages/trpc/src/procedures/labels/getLabels.ts b/packages/trpc/src/procedures/labels/getLabels.ts index 2e7291805..72de909f4 100644 --- a/packages/trpc/src/procedures/labels/getLabels.ts +++ b/packages/trpc/src/procedures/labels/getLabels.ts @@ -1,11 +1,11 @@ import { prisma } from "@recipesage/prisma"; import { publicProcedure } from "../../trpc"; -import { validateSession } from "@recipesage/util/server/general"; +import { validateTrpcSession } from "@recipesage/util/server/general"; import { labelSummary } from "@recipesage/prisma"; export const getLabels = publicProcedure.query(async ({ ctx }) => { const session = ctx.session; - validateSession(session); + validateTrpcSession(session); const labels = await prisma.label.findMany({ where: { diff --git a/packages/trpc/src/procedures/labels/getLabelsByUserId.ts b/packages/trpc/src/procedures/labels/getLabelsByUserId.ts index 9db48e755..dcdb84ae3 100644 --- a/packages/trpc/src/procedures/labels/getLabelsByUserId.ts +++ b/packages/trpc/src/procedures/labels/getLabelsByUserId.ts @@ -1,6 +1,6 @@ import { publicProcedure } from "../../trpc"; import { getVisibleLabels } from "@recipesage/util/server/db"; -import { validateSession } from "@recipesage/util/server/general"; +import { validateTrpcSession } from "@recipesage/util/server/general"; import { z } from "zod"; export const getLabelsByUserId = publicProcedure @@ -11,7 +11,7 @@ export const getLabelsByUserId = publicProcedure ) .query(async ({ ctx, input }) => { const session = ctx.session; - validateSession(session); + validateTrpcSession(session); const visibleLabels = await getVisibleLabels(session.userId, { userIds: input.userIds, diff --git a/packages/trpc/src/procedures/labels/mergeLabels.ts b/packages/trpc/src/procedures/labels/mergeLabels.ts index 2c937f21f..39db203f4 100644 --- a/packages/trpc/src/procedures/labels/mergeLabels.ts +++ b/packages/trpc/src/procedures/labels/mergeLabels.ts @@ -1,6 +1,6 @@ import { prisma } from "@recipesage/prisma"; import { publicProcedure } from "../../trpc"; -import { validateSession } from "@recipesage/util/server/general"; +import { validateTrpcSession } from "@recipesage/util/server/general"; import { z } from "zod"; import { TRPCError } from "@trpc/server"; @@ -13,7 +13,7 @@ export const mergeLabels = publicProcedure ) .mutation(async ({ ctx, input }) => { const session = ctx.session; - validateSession(session); + validateTrpcSession(session); if (input.sourceId === input.targetId) { throw new TRPCError({ diff --git a/packages/trpc/src/procedures/labels/updateLabel.ts b/packages/trpc/src/procedures/labels/updateLabel.ts index 2b591c4c1..6ebda4078 100644 --- a/packages/trpc/src/procedures/labels/updateLabel.ts +++ b/packages/trpc/src/procedures/labels/updateLabel.ts @@ -1,6 +1,6 @@ import { prisma } from "@recipesage/prisma"; import { publicProcedure } from "../../trpc"; -import { validateSession } from "@recipesage/util/server/general"; +import { validateTrpcSession } from "@recipesage/util/server/general"; import { labelSummary } from "@recipesage/prisma"; import { z } from "zod"; import { TRPCError } from "@trpc/server"; @@ -15,7 +15,7 @@ export const updateLabel = publicProcedure ) .mutation(async ({ ctx, input }) => { const session = ctx.session; - validateSession(session); + validateTrpcSession(session); const existingLabel = await prisma.label.findFirst({ where: { diff --git a/packages/trpc/src/procedures/mealPlans/createMealPlan.ts b/packages/trpc/src/procedures/mealPlans/createMealPlan.ts new file mode 100644 index 000000000..b11ab7359 --- /dev/null +++ b/packages/trpc/src/procedures/mealPlans/createMealPlan.ts @@ -0,0 +1,67 @@ +import { publicProcedure } from "../../trpc"; +import { + WSBoardcastEventType, + broadcastWSEvent, + validateTrpcSession, +} from "@recipesage/util/server/general"; +import { prisma } from "@recipesage/prisma"; +import { z } from "zod"; +import { TRPCError } from "@trpc/server"; + +export const createMealPlan = publicProcedure + .input( + z.object({ + title: z.string(), + collaboratorUserIds: z.array(z.string().uuid()), + }), + ) + .mutation(async ({ ctx, input }) => { + const session = ctx.session; + validateTrpcSession(session); + + const collaboratorUsers = await prisma.user.findMany({ + where: { + id: { + in: input.collaboratorUserIds, + }, + }, + select: { + id: true, + }, + }); + + if (collaboratorUsers.length < input.collaboratorUserIds.length) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "One or more of the collaborators you specified are not valid", + }); + } + + const createdMealPlan = await prisma.mealPlan.create({ + data: { + title: input.title, + userId: session.userId, + collaboratorUsers: { + createMany: { + data: collaboratorUsers.map((collaboratorUser) => ({ + userId: collaboratorUser.id, + })), + }, + }, + }, + }); + + const reference = crypto.randomUUID(); + const notifyUsers = [createdMealPlan.userId, ...input.collaboratorUserIds]; + for (const notifyUser of notifyUsers) { + broadcastWSEvent(notifyUser, WSBoardcastEventType.MealPlanUpdated, { + reference, + mealPlanId: createdMealPlan.id, + }); + } + + return { + reference, + id: createdMealPlan.id, + }; + }); diff --git a/packages/trpc/src/procedures/mealPlans/createMealPlanItem.ts b/packages/trpc/src/procedures/mealPlans/createMealPlanItem.ts new file mode 100644 index 000000000..0a73b00eb --- /dev/null +++ b/packages/trpc/src/procedures/mealPlans/createMealPlanItem.ts @@ -0,0 +1,68 @@ +import { publicProcedure } from "../../trpc"; +import { + WSBoardcastEventType, + broadcastWSEvent, + validateTrpcSession, +} from "@recipesage/util/server/general"; +import { prisma } from "@recipesage/prisma"; +import { z } from "zod"; +import { TRPCError } from "@trpc/server"; +import { + MealPlanAccessLevel, + getAccessToMealPlan, +} from "@recipesage/util/server/db"; + +export const createMealPlanItem = publicProcedure + .input( + z.object({ + mealPlanId: z.string().uuid(), + title: z.string(), + scheduledDate: z.string().regex(/^\d{4}-\d{2}-\d{2}$/), + meal: z.union([ + z.literal("breakfast"), + z.literal("lunch"), + z.literal("dinner"), + z.literal("snacks"), + z.literal("other"), + ]), + recipeId: z.string().uuid().nullable(), + }), + ) + .mutation(async ({ ctx, input }) => { + const session = ctx.session; + validateTrpcSession(session); + + const access = await getAccessToMealPlan(session.userId, input.mealPlanId); + + if (access.level === MealPlanAccessLevel.None) { + throw new TRPCError({ + code: "NOT_FOUND", + message: + "Meal plan with that id does not exist or you do not have access", + }); + } + + const createdMealPlanItem = await prisma.mealPlanItem.create({ + data: { + mealPlanId: input.mealPlanId, + title: input.title, + userId: session.userId, + scheduledDate: new Date(input.scheduledDate), + meal: input.meal, + recipeId: input.recipeId, + }, + }); + + const reference = crypto.randomUUID(); + for (const subscriberId of access.subscriberIds) { + broadcastWSEvent(subscriberId, WSBoardcastEventType.MealPlanUpdated, { + reference, + mealPlanId: input.mealPlanId, + }); + } + + return { + reference, + id: createdMealPlanItem.id, + }; + }); diff --git a/packages/trpc/src/procedures/mealPlans/createMealPlanItems.ts b/packages/trpc/src/procedures/mealPlans/createMealPlanItems.ts new file mode 100644 index 000000000..d84ce83b1 --- /dev/null +++ b/packages/trpc/src/procedures/mealPlans/createMealPlanItems.ts @@ -0,0 +1,71 @@ +import { publicProcedure } from "../../trpc"; +import { + WSBoardcastEventType, + broadcastWSEvent, + validateTrpcSession, +} from "@recipesage/util/server/general"; +import { prisma } from "@recipesage/prisma"; +import { z } from "zod"; +import { TRPCError } from "@trpc/server"; +import { + MealPlanAccessLevel, + getAccessToMealPlan, +} from "@recipesage/util/server/db"; + +export const createMealPlanItems = publicProcedure + .input( + z.object({ + mealPlanId: z.string().uuid(), + items: z.array( + z.object({ + title: z.string(), + scheduledDate: z.string().regex(/^\d{4}-\d{2}-\d{2}$/), + meal: z.union([ + z.literal("breakfast"), + z.literal("lunch"), + z.literal("dinner"), + z.literal("snacks"), + z.literal("other"), + ]), + recipeId: z.string().uuid().nullable(), + }), + ), + }), + ) + .mutation(async ({ ctx, input }) => { + const session = ctx.session; + validateTrpcSession(session); + + const access = await getAccessToMealPlan(session.userId, input.mealPlanId); + + if (access.level === MealPlanAccessLevel.None) { + throw new TRPCError({ + code: "NOT_FOUND", + message: + "Meal plan with that id does not exist or you do not have access", + }); + } + + await prisma.mealPlanItem.createMany({ + data: input.items.map((el) => ({ + mealPlanId: input.mealPlanId, + title: el.title, + userId: session.userId, + scheduledDate: new Date(el.scheduledDate), + meal: el.meal, + recipeId: el.recipeId, + })), + }); + + const reference = crypto.randomUUID(); + for (const subscriberId of access.subscriberIds) { + broadcastWSEvent(subscriberId, WSBoardcastEventType.MealPlanUpdated, { + reference, + mealPlanId: input.mealPlanId, + }); + } + + return { + reference, + }; + }); diff --git a/packages/trpc/src/procedures/mealPlans/deleteMealPlan.ts b/packages/trpc/src/procedures/mealPlans/deleteMealPlan.ts new file mode 100644 index 000000000..913308194 --- /dev/null +++ b/packages/trpc/src/procedures/mealPlans/deleteMealPlan.ts @@ -0,0 +1,52 @@ +import { publicProcedure } from "../../trpc"; +import { + WSBoardcastEventType, + broadcastWSEvent, + validateTrpcSession, +} from "@recipesage/util/server/general"; +import { prisma } from "@recipesage/prisma"; +import { z } from "zod"; +import { TRPCError } from "@trpc/server"; +import { + MealPlanAccessLevel, + getAccessToMealPlan, +} from "@recipesage/util/server/db"; + +export const deleteMealPlan = publicProcedure + .input( + z.object({ + id: z.string().uuid(), + }), + ) + .mutation(async ({ ctx, input }) => { + const session = ctx.session; + validateTrpcSession(session); + + const access = await getAccessToMealPlan(session.userId, input.id); + + if (access.level !== MealPlanAccessLevel.Owner) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "Meal plan with that id does not exist or you do not own it", + }); + } + + await prisma.mealPlan.delete({ + where: { + id: input.id, + }, + }); + + const reference = crypto.randomUUID(); + for (const subscriberId of access.subscriberIds) { + broadcastWSEvent(subscriberId, WSBoardcastEventType.MealPlanUpdated, { + reference, + mealPlanId: input.id, + }); + } + + return { + reference, + id: input.id, + }; + }); diff --git a/packages/trpc/src/procedures/mealPlans/deleteMealPlanItem.ts b/packages/trpc/src/procedures/mealPlans/deleteMealPlanItem.ts new file mode 100644 index 000000000..32d3fc91e --- /dev/null +++ b/packages/trpc/src/procedures/mealPlans/deleteMealPlanItem.ts @@ -0,0 +1,71 @@ +import { publicProcedure } from "../../trpc"; +import { + WSBoardcastEventType, + broadcastWSEvent, + validateTrpcSession, +} from "@recipesage/util/server/general"; +import { prisma } from "@recipesage/prisma"; +import { z } from "zod"; +import { TRPCError } from "@trpc/server"; +import { + MealPlanAccessLevel, + getAccessToMealPlan, +} from "@recipesage/util/server/db"; + +export const deleteMealPlanItem = publicProcedure + .input( + z.object({ + id: z.string().uuid(), + }), + ) + .mutation(async ({ ctx, input }) => { + const session = ctx.session; + validateTrpcSession(session); + + const mealPlanItem = await prisma.mealPlanItem.findUnique({ + where: { + id: input.id, + }, + select: { + mealPlanId: true, + }, + }); + + if (!mealPlanItem) { + throw new TRPCError({ + code: "NOT_FOUND", + }); + } + + const access = await getAccessToMealPlan( + session.userId, + mealPlanItem.mealPlanId, + ); + + if (access.level === MealPlanAccessLevel.None) { + throw new TRPCError({ + code: "NOT_FOUND", + message: + "Meal plan with that id does not exist or you do not have access", + }); + } + + const deletedMealPlanItem = await prisma.mealPlanItem.delete({ + where: { + id: input.id, + }, + }); + + const reference = crypto.randomUUID(); + for (const subscriberId of access.subscriberIds) { + broadcastWSEvent(subscriberId, WSBoardcastEventType.MealPlanUpdated, { + reference, + mealPlanId: mealPlanItem.mealPlanId, + }); + } + + return { + reference, + id: deletedMealPlanItem.id, + }; + }); diff --git a/packages/trpc/src/procedures/mealPlans/deleteMealPlanItems.ts b/packages/trpc/src/procedures/mealPlans/deleteMealPlanItems.ts new file mode 100644 index 000000000..cb5974fb6 --- /dev/null +++ b/packages/trpc/src/procedures/mealPlans/deleteMealPlanItems.ts @@ -0,0 +1,73 @@ +import { publicProcedure } from "../../trpc"; +import { + WSBoardcastEventType, + broadcastWSEvent, + validateTrpcSession, +} from "@recipesage/util/server/general"; +import { prisma } from "@recipesage/prisma"; +import { z } from "zod"; +import { TRPCError } from "@trpc/server"; +import { + MealPlanAccessLevel, + getAccessToMealPlan, +} from "@recipesage/util/server/db"; + +export const deleteMealPlanItems = publicProcedure + .input( + z.object({ + mealPlanId: z.string().uuid(), + ids: z.array(z.string().uuid()), + }), + ) + .mutation(async ({ ctx, input }) => { + const session = ctx.session; + validateTrpcSession(session); + + const mealPlanItems = await prisma.mealPlanItem.findMany({ + where: { + id: { + in: input.ids, + }, + mealPlanId: input.mealPlanId, + }, + select: { + mealPlanId: true, + }, + }); + + if (mealPlanItems.length !== input.ids.length) { + throw new TRPCError({ + code: "NOT_FOUND", + message: + "One or more of the items you've passed do not exist, or do not belong to the meal plan id", + }); + } + + const access = await getAccessToMealPlan(session.userId, input.mealPlanId); + + if (access.level === MealPlanAccessLevel.None) { + throw new TRPCError({ + code: "NOT_FOUND", + message: + "Meal plan with that id does not exist or you do not have access", + }); + } + + await prisma.mealPlanItem.deleteMany({ + where: { + id: { in: input.ids }, + }, + }); + + const reference = crypto.randomUUID(); + for (const subscriberId of access.subscriberIds) { + broadcastWSEvent(subscriberId, WSBoardcastEventType.MealPlanUpdated, { + reference, + mealPlanId: input.mealPlanId, + }); + } + + return { + reference, + }; + }); diff --git a/packages/trpc/src/procedures/mealPlans/detachMealPlan.ts b/packages/trpc/src/procedures/mealPlans/detachMealPlan.ts new file mode 100644 index 000000000..e5c7a594e --- /dev/null +++ b/packages/trpc/src/procedures/mealPlans/detachMealPlan.ts @@ -0,0 +1,56 @@ +import { publicProcedure } from "../../trpc"; +import { + WSBoardcastEventType, + broadcastWSEvent, + validateTrpcSession, +} from "@recipesage/util/server/general"; +import { prisma } from "@recipesage/prisma"; +import { z } from "zod"; +import { TRPCError } from "@trpc/server"; +import { + MealPlanAccessLevel, + getAccessToMealPlan, +} from "@recipesage/util/server/db"; + +export const detachMealPlan = publicProcedure + .input( + z.object({ + id: z.string().uuid(), + }), + ) + .mutation(async ({ ctx, input }) => { + const session = ctx.session; + validateTrpcSession(session); + + const access = await getAccessToMealPlan(session.userId, input.id); + + if (access.level !== MealPlanAccessLevel.Collaborator) { + throw new TRPCError({ + code: "NOT_FOUND", + message: + "Meal plan with that id does not exist or you are not a collaborator for it", + }); + } + + await prisma.mealPlanCollaborator.delete({ + where: { + mealPlanId_userId: { + mealPlanId: input.id, + userId: session.userId, + }, + }, + }); + + const reference = crypto.randomUUID(); + for (const subscriberId of access.subscriberIds) { + broadcastWSEvent(subscriberId, WSBoardcastEventType.MealPlanUpdated, { + reference, + mealPlanId: input.id, + }); + } + + return { + reference, + id: input.id, + }; + }); diff --git a/packages/trpc/src/procedures/mealPlans/getMealPlan.ts b/packages/trpc/src/procedures/mealPlans/getMealPlan.ts new file mode 100644 index 000000000..62b55bc25 --- /dev/null +++ b/packages/trpc/src/procedures/mealPlans/getMealPlan.ts @@ -0,0 +1,52 @@ +import { publicProcedure } from "../../trpc"; +import { validateTrpcSession } from "@recipesage/util/server/general"; +import { mealPlanSummary, prisma } from "@recipesage/prisma"; +import { z } from "zod"; +import { TRPCError } from "@trpc/server"; + +export const getMealPlan = publicProcedure + .input( + z.object({ + id: z.string().uuid(), + }), + ) + .query(async ({ ctx, input }) => { + const session = ctx.session; + validateTrpcSession(session); + + const collabRelationships = await prisma.mealPlanCollaborator.findMany({ + where: { + mealPlanId: input.id, + userId: session.userId, + }, + select: { + mealPlanId: true, + }, + }); + + const mealPlan = await prisma.mealPlan.findFirst({ + where: { + OR: [ + { + id: input.id, + userId: session.userId, + }, + { + id: { + in: collabRelationships.map((el) => el.mealPlanId), + }, + }, + ], + }, + ...mealPlanSummary, + }); + + if (!mealPlan) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "Meal plan with that id not found or you do not have access", + }); + } + + return mealPlan; + }); diff --git a/packages/trpc/src/procedures/mealPlans/getMealPlanItems.ts b/packages/trpc/src/procedures/mealPlans/getMealPlanItems.ts new file mode 100644 index 000000000..bc9eeaa79 --- /dev/null +++ b/packages/trpc/src/procedures/mealPlans/getMealPlanItems.ts @@ -0,0 +1,52 @@ +import { publicProcedure } from "../../trpc"; +import { validateTrpcSession } from "@recipesage/util/server/general"; +import { + MealPlanItemSummary, + mealPlanItemSummary, + prisma, +} from "@recipesage/prisma"; +import { z } from "zod"; +import { TRPCError } from "@trpc/server"; +import { + MealPlanAccessLevel, + convertPrismaDateToDatestamp, + getAccessToMealPlan, +} from "@recipesage/util/server/db"; + +export const getMealPlanItems = publicProcedure + .input( + z.object({ + mealPlanId: z.string().uuid(), + limit: z.number().min(1).max(4000).default(1000), + }), + ) + .query(async ({ ctx, input }) => { + const session = ctx.session; + validateTrpcSession(session); + + const access = await getAccessToMealPlan(session.userId, input.mealPlanId); + + if (access.level === MealPlanAccessLevel.None) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "Meal plan not found or you do not have access", + }); + } + + const mealPlanItems = await prisma.mealPlanItem.findMany({ + where: { + mealPlanId: input.mealPlanId, + }, + ...mealPlanItemSummary, + orderBy: { + scheduled: "desc", + }, + take: input.limit, + }); + + const resultMealPlanItems = mealPlanItems.map((mealPlanItem) => + convertPrismaDateToDatestamp(mealPlanItem, "scheduledDate"), + ); + + return resultMealPlanItems satisfies MealPlanItemSummary[]; + }); diff --git a/packages/trpc/src/procedures/mealPlans/getMealPlans.ts b/packages/trpc/src/procedures/mealPlans/getMealPlans.ts new file mode 100644 index 000000000..69f23c674 --- /dev/null +++ b/packages/trpc/src/procedures/mealPlans/getMealPlans.ts @@ -0,0 +1,38 @@ +import { publicProcedure } from "../../trpc"; +import { validateTrpcSession } from "@recipesage/util/server/general"; +import { mealPlanSummary, prisma } from "@recipesage/prisma"; + +export const getMealPlans = publicProcedure.query(async ({ ctx }) => { + const session = ctx.session; + validateTrpcSession(session); + + const collabRelationships = await prisma.mealPlanCollaborator.findMany({ + where: { + userId: session.userId, + }, + select: { + mealPlanId: true, + }, + }); + + const mealPlans = await prisma.mealPlan.findMany({ + where: { + OR: [ + { + userId: session.userId, + }, + { + id: { + in: collabRelationships.map((el) => el.mealPlanId), + }, + }, + ], + }, + ...mealPlanSummary, + orderBy: { + createdAt: "desc", + }, + }); + + return mealPlans; +}); diff --git a/packages/trpc/src/procedures/mealPlans/updateMealPlan.ts b/packages/trpc/src/procedures/mealPlans/updateMealPlan.ts new file mode 100644 index 000000000..eaa17102c --- /dev/null +++ b/packages/trpc/src/procedures/mealPlans/updateMealPlan.ts @@ -0,0 +1,107 @@ +import { publicProcedure } from "../../trpc"; +import { + WSBoardcastEventType, + broadcastWSEvent, + validateTrpcSession, +} from "@recipesage/util/server/general"; +import { prisma } from "@recipesage/prisma"; +import { z } from "zod"; +import { TRPCError } from "@trpc/server"; + +export const updateMealPlan = publicProcedure + .input( + z.object({ + id: z.string().uuid(), + title: z.string(), + collaboratorUserIds: z.array(z.string().uuid()), + }), + ) + .mutation(async ({ ctx, input }) => { + const session = ctx.session; + validateTrpcSession(session); + + const collaboratorUsers = await prisma.user.findMany({ + where: { + id: { + in: input.collaboratorUserIds, + }, + }, + select: { + id: true, + }, + }); + + if (collaboratorUsers.length < input.collaboratorUserIds.length) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "One or more of the collaborators you specified are not valid", + }); + } + + const mealPlan = await prisma.mealPlan.findUnique({ + where: { + id: input.id, + userId: session.userId, + }, + select: { + id: true, + collaboratorUsers: { + select: { + userId: true, + }, + }, + }, + }); + + if (!mealPlan) { + throw new TRPCError({ + code: "NOT_FOUND", + message: + "Meal plan with that id does not exist or you do not have access", + }); + } + + await prisma.mealPlanCollaborator.deleteMany({ + where: { + mealPlanId: input.id, + }, + }); + + const updatedMealPlan = await prisma.mealPlan.update({ + where: { + id: input.id, + }, + data: { + title: input.title, + userId: session.userId, + collaboratorUsers: { + createMany: { + data: collaboratorUsers.map((collaboratorUser) => ({ + userId: collaboratorUser.id, + })), + }, + }, + }, + }); + + const reference = crypto.randomUUID(); + const subscriberIds = [ + ...new Set([ + updatedMealPlan.userId, + // We need to notify both the old collaborators and the new collaborators of the update + ...input.collaboratorUserIds, + ...mealPlan.collaboratorUsers.map((el) => el.userId), + ]), + ]; + for (const subscriberId of subscriberIds) { + broadcastWSEvent(subscriberId, WSBoardcastEventType.MealPlanUpdated, { + reference, + mealPlanId: updatedMealPlan.id, + }); + } + + return { + reference, + id: updatedMealPlan.id, + }; + }); diff --git a/packages/trpc/src/procedures/mealPlans/updateMealPlanItem.ts b/packages/trpc/src/procedures/mealPlans/updateMealPlanItem.ts new file mode 100644 index 000000000..50a40e2c6 --- /dev/null +++ b/packages/trpc/src/procedures/mealPlans/updateMealPlanItem.ts @@ -0,0 +1,88 @@ +import { publicProcedure } from "../../trpc"; +import { + WSBoardcastEventType, + broadcastWSEvent, + validateTrpcSession, +} from "@recipesage/util/server/general"; +import { prisma } from "@recipesage/prisma"; +import { z } from "zod"; +import { TRPCError } from "@trpc/server"; +import { + MealPlanAccessLevel, + getAccessToMealPlan, +} from "@recipesage/util/server/db"; + +export const updateMealPlanItem = publicProcedure + .input( + z.object({ + id: z.string().uuid(), + title: z.string(), + scheduledDate: z.string().regex(/^\d{4}-\d{2}-\d{2}$/), + meal: z.union([ + z.literal("breakfast"), + z.literal("lunch"), + z.literal("dinner"), + z.literal("snacks"), + z.literal("other"), + ]), + recipeId: z.string().uuid().nullable(), + }), + ) + .mutation(async ({ ctx, input }) => { + const session = ctx.session; + validateTrpcSession(session); + + const mealPlanItem = await prisma.mealPlanItem.findUnique({ + where: { + id: input.id, + }, + select: { + mealPlanId: true, + }, + }); + + if (!mealPlanItem) { + throw new TRPCError({ + code: "NOT_FOUND", + }); + } + + const access = await getAccessToMealPlan( + session.userId, + mealPlanItem.mealPlanId, + ); + + if (access.level === MealPlanAccessLevel.None) { + throw new TRPCError({ + code: "NOT_FOUND", + message: + "Meal plan with that id does not exist or you do not have access", + }); + } + + const updatedMealPlanItem = await prisma.mealPlanItem.update({ + where: { + id: input.id, + }, + data: { + title: input.title, + scheduled: null, // Remove legacy scheduling + scheduledDate: new Date(input.scheduledDate), + meal: input.meal, + recipeId: input.recipeId, + }, + }); + + const reference = crypto.randomUUID(); + for (const subscriberId of access.subscriberIds) { + broadcastWSEvent(subscriberId, WSBoardcastEventType.MealPlanUpdated, { + reference, + mealPlanId: mealPlanItem.mealPlanId, + }); + } + + return { + reference, + id: updatedMealPlanItem.id, + }; + }); diff --git a/packages/trpc/src/procedures/mealPlans/updateMealPlanItems.ts b/packages/trpc/src/procedures/mealPlans/updateMealPlanItems.ts new file mode 100644 index 000000000..faaf10fd3 --- /dev/null +++ b/packages/trpc/src/procedures/mealPlans/updateMealPlanItems.ts @@ -0,0 +1,101 @@ +import { publicProcedure } from "../../trpc"; +import { + WSBoardcastEventType, + broadcastWSEvent, + validateTrpcSession, +} from "@recipesage/util/server/general"; +import { prisma } from "@recipesage/prisma"; +import { z } from "zod"; +import { TRPCError } from "@trpc/server"; +import { + MealPlanAccessLevel, + getAccessToMealPlan, +} from "@recipesage/util/server/db"; + +export const updateMealPlanItems = publicProcedure + .input( + z.object({ + mealPlanId: z.string().uuid(), + items: z + .array( + z.object({ + id: z.string().uuid(), + title: z.string(), + scheduledDate: z.string().regex(/^\d{4}-\d{2}-\d{2}$/), + meal: z.union([ + z.literal("breakfast"), + z.literal("lunch"), + z.literal("dinner"), + z.literal("snacks"), + z.literal("other"), + ]), + recipeId: z.string().uuid().nullable(), + }), + ) + .min(1) + .max(100), // These will be individual DB calls, so cap number of allowed items + }), + ) + .mutation(async ({ ctx, input }) => { + const session = ctx.session; + validateTrpcSession(session); + + const mealPlanItems = await prisma.mealPlanItem.findMany({ + where: { + id: { + in: input.items.map((el) => el.id), + }, + mealPlanId: input.mealPlanId, + }, + select: { + id: true, + }, + }); + + if (mealPlanItems.length !== input.items.length) { + throw new TRPCError({ + code: "NOT_FOUND", + message: + "One or more of the items you've passed do not exist, or do not belong to the meal plan id", + }); + } + + const access = await getAccessToMealPlan(session.userId, input.mealPlanId); + + if (access.level === MealPlanAccessLevel.None) { + throw new TRPCError({ + code: "NOT_FOUND", + message: + "Meal plan with that id does not exist or you do not have access", + }); + } + + await prisma.$transaction(async (tx) => { + for (const item of input.items) { + await tx.mealPlanItem.updateMany({ + where: { + id: item.id, + }, + data: { + title: item.title, + scheduled: null, // Remove legacy scheduling + scheduledDate: new Date(item.scheduledDate), + meal: item.meal, + recipeId: item.recipeId, + }, + }); + } + }); + + const reference = crypto.randomUUID(); + for (const subscriberId of access.subscriberIds) { + broadcastWSEvent(subscriberId, WSBoardcastEventType.MealPlanUpdated, { + reference, + mealPlanId: input.mealPlanId, + }); + } + + return { + reference, + }; + }); diff --git a/packages/trpc/src/procedures/ml/getRecipeFromPDF.ts b/packages/trpc/src/procedures/ml/getRecipeFromPDF.ts index 843408c76..d217d5ee1 100644 --- a/packages/trpc/src/procedures/ml/getRecipeFromPDF.ts +++ b/packages/trpc/src/procedures/ml/getRecipeFromPDF.ts @@ -18,7 +18,7 @@ export const getRecipeFromPDF = publicProcedure }); } - const pdf = new Uint8Array(Buffer.from(input.pdf, "base64")); + const pdf = Buffer.from(input.pdf, "base64"); const recognizedRecipe = await pdfToRecipe(pdf, 2); if (!recognizedRecipe) { diff --git a/packages/trpc/src/procedures/recipes/createRecipe.ts b/packages/trpc/src/procedures/recipes/createRecipe.ts index 34ced44cf..92f7a18bc 100644 --- a/packages/trpc/src/procedures/recipes/createRecipe.ts +++ b/packages/trpc/src/procedures/recipes/createRecipe.ts @@ -6,7 +6,7 @@ import { MULTIPLE_IMAGES_UNLOCKED_LIMIT, userHasCapability, } from "@recipesage/util/server/capabilities"; -import { validateSession } from "@recipesage/util/server/general"; +import { validateTrpcSession } from "@recipesage/util/server/general"; import { z } from "zod"; import { TRPCError } from "@trpc/server"; @@ -31,7 +31,7 @@ export const createRecipe = publicProcedure ) .mutation(async ({ ctx, input }) => { const session = ctx.session; - validateSession(session); + validateTrpcSession(session); const labelIds = input.labelIds || []; const doesNotOwnAssignedLabel = !!(await prisma.label.findFirst({ diff --git a/packages/trpc/src/procedures/recipes/deleteRecipe.ts b/packages/trpc/src/procedures/recipes/deleteRecipe.ts index 661e48913..7c18797cf 100644 --- a/packages/trpc/src/procedures/recipes/deleteRecipe.ts +++ b/packages/trpc/src/procedures/recipes/deleteRecipe.ts @@ -2,7 +2,7 @@ import { prisma } from "@recipesage/prisma"; import { publicProcedure } from "../../trpc"; import { z } from "zod"; import { TRPCError } from "@trpc/server"; -import { validateSession } from "@recipesage/util/server/general"; +import { validateTrpcSession } from "@recipesage/util/server/general"; import { deleteRecipes } from "@recipesage/util/server/search"; export const deleteRecipe = publicProcedure @@ -13,7 +13,7 @@ export const deleteRecipe = publicProcedure ) .mutation(async ({ ctx, input }) => { const session = ctx.session; - validateSession(session); + validateTrpcSession(session); const recipe = await prisma.recipe.findUnique({ where: { diff --git a/packages/trpc/src/procedures/recipes/getRecipesByTitle.ts b/packages/trpc/src/procedures/recipes/getRecipesByTitle.ts index b930c75f7..306d1ce52 100644 --- a/packages/trpc/src/procedures/recipes/getRecipesByTitle.ts +++ b/packages/trpc/src/procedures/recipes/getRecipesByTitle.ts @@ -1,7 +1,7 @@ import { publicProcedure } from "../../trpc"; import { z } from "zod"; import { prisma } from "@recipesage/prisma"; -import { validateSession } from "@recipesage/util/server/general"; +import { validateTrpcSession } from "@recipesage/util/server/general"; import { recipeSummaryLite } from "@recipesage/prisma"; export const getRecipesByTitle = publicProcedure @@ -12,7 +12,7 @@ export const getRecipesByTitle = publicProcedure ) .query(async ({ ctx, input }) => { const session = ctx.session; - validateSession(session); + validateTrpcSession(session); const recipes = prisma.recipe.findMany({ where: { diff --git a/packages/trpc/src/procedures/recipes/getUniqueRecipeTitle.ts b/packages/trpc/src/procedures/recipes/getUniqueRecipeTitle.ts index cc6fa59ab..c60de020c 100644 --- a/packages/trpc/src/procedures/recipes/getUniqueRecipeTitle.ts +++ b/packages/trpc/src/procedures/recipes/getUniqueRecipeTitle.ts @@ -2,7 +2,7 @@ import { publicProcedure } from "../../trpc"; import { z } from "zod"; import { prisma } from "@recipesage/prisma"; import { - validateSession, + validateTrpcSession, stripNumberedRecipeTitle, } from "@recipesage/util/server/general"; import { recipeSummaryLite } from "@recipesage/prisma"; @@ -22,7 +22,7 @@ export const getUniqueRecipeTitle = publicProcedure ) .query(async ({ ctx, input }) => { const session = ctx.session; - validateSession(session); + validateTrpcSession(session); const strippedRecipeTitle = stripNumberedRecipeTitle(input.title); diff --git a/packages/trpc/src/procedures/recipes/updateRecipe.ts b/packages/trpc/src/procedures/recipes/updateRecipe.ts index 7da267b4a..59da992ca 100644 --- a/packages/trpc/src/procedures/recipes/updateRecipe.ts +++ b/packages/trpc/src/procedures/recipes/updateRecipe.ts @@ -2,7 +2,7 @@ import { prisma } from "@recipesage/prisma"; import { publicProcedure } from "../../trpc"; import { z } from "zod"; import { TRPCError } from "@trpc/server"; -import { validateSession } from "@recipesage/util/server/general"; +import { validateTrpcSession } from "@recipesage/util/server/general"; import { Capabilities, MULTIPLE_IMAGES_UNLOCKED_LIMIT, @@ -32,7 +32,7 @@ export const updateRecipe = publicProcedure ) .mutation(async ({ ctx, input }) => { const session = ctx.session; - validateSession(session); + validateTrpcSession(session); const initialRecipe = await prisma.recipe.findUnique({ where: { diff --git a/packages/trpc/src/procedures/users/getMe.ts b/packages/trpc/src/procedures/users/getMe.ts index 849f72ea2..2ed47d049 100644 --- a/packages/trpc/src/procedures/users/getMe.ts +++ b/packages/trpc/src/procedures/users/getMe.ts @@ -1,11 +1,11 @@ import { prisma } from "@recipesage/prisma"; import { publicProcedure } from "../../trpc"; import { userPublic } from "@recipesage/prisma"; -import { validateSession } from "@recipesage/util/server/general"; +import { validateTrpcSession } from "@recipesage/util/server/general"; export const getMe = publicProcedure.query(async ({ ctx }) => { const session = ctx.session; - validateSession(session); + validateTrpcSession(session); const profile = await prisma.user.findUnique({ where: { diff --git a/packages/trpc/src/procedures/users/getPreferences.ts b/packages/trpc/src/procedures/users/getPreferences.ts index 1574d6b9e..a70d82599 100644 --- a/packages/trpc/src/procedures/users/getPreferences.ts +++ b/packages/trpc/src/procedures/users/getPreferences.ts @@ -1,11 +1,11 @@ import { prisma } from "@recipesage/prisma"; import { publicProcedure } from "../../trpc"; import { AppPreferenceTypes } from "@recipesage/util/shared"; -import { validateSession } from "@recipesage/util/server/general"; +import { validateTrpcSession } from "@recipesage/util/server/general"; export const getPreferences = publicProcedure.query(async ({ ctx }) => { const session = ctx.session; - validateSession(session); + validateTrpcSession(session); const user = await prisma.user.findUniqueOrThrow({ where: { diff --git a/packages/trpc/src/procedures/users/updatePreferences.ts b/packages/trpc/src/procedures/users/updatePreferences.ts index 3775db2c5..13b4c7422 100644 --- a/packages/trpc/src/procedures/users/updatePreferences.ts +++ b/packages/trpc/src/procedures/users/updatePreferences.ts @@ -1,6 +1,6 @@ import { prisma } from "@recipesage/prisma"; import { publicProcedure } from "../../trpc"; -import { validateSession } from "@recipesage/util/server/general"; +import { validateTrpcSession } from "@recipesage/util/server/general"; import { z } from "zod"; import { AppPreferenceTypes, @@ -71,7 +71,7 @@ export const updatePreferences = publicProcedure ) .mutation(async ({ ctx, input }) => { const session = ctx.session; - validateSession(session); + validateTrpcSession(session); await prisma.user.update({ where: { diff --git a/packages/util/server/src/capabilities/subscriptionsForUser.ts b/packages/util/server/src/capabilities/subscriptionsForUser.ts index de89e3197..de1349314 100644 --- a/packages/util/server/src/capabilities/subscriptionsForUser.ts +++ b/packages/util/server/src/capabilities/subscriptionsForUser.ts @@ -14,9 +14,6 @@ export const subscriptionsForUser = async ( const subscriptions = prisma.userSubscription.findMany({ where: { userId, - name: { - not: null, - }, OR: [ { expires: { diff --git a/packages/util/server/src/db/convertPrismaDateToDatestamp.ts b/packages/util/server/src/db/convertPrismaDateToDatestamp.ts new file mode 100644 index 000000000..cbffb7e49 --- /dev/null +++ b/packages/util/server/src/db/convertPrismaDateToDatestamp.ts @@ -0,0 +1,17 @@ +export const convertPrismaDateToDatestamp = ( + obj: T, + fieldName: Z, +): Omit & { + [key in Z]: string; +} => { + const date = obj[fieldName]; + + if (!(date instanceof Date)) { + throw new Error("convertPrismaDate called with non-date property"); + } + + return { + ...obj, + [fieldName]: `${date.getUTCFullYear()}-${String(date.getUTCMonth() + 1).padStart(2, "0")}-${String(date.getUTCDate()).padStart(2, "0")}`, + }; +}; diff --git a/packages/util/server/src/db/getAccessToMealPlan.ts b/packages/util/server/src/db/getAccessToMealPlan.ts new file mode 100644 index 000000000..86dbce4e7 --- /dev/null +++ b/packages/util/server/src/db/getAccessToMealPlan.ts @@ -0,0 +1,82 @@ +import { prisma } from "@recipesage/prisma"; + +export enum MealPlanAccessLevel { + None = "none", + Owner = "owner", + Collaborator = "collaborator", +} + +type AccessResultType = + | { + level: MealPlanAccessLevel.None; + ownerId: null; + collaboratorIds: null; + subscriberIds: null; + } + | { + level: MealPlanAccessLevel.Owner | MealPlanAccessLevel.Collaborator; + ownerId: string; + collaboratorIds: string[]; + subscriberIds: string[]; + }; + +export const getAccessToMealPlan = async ( + userId: string, + mealPlanId: string, +): Promise => { + const mealPlan = await prisma.mealPlan.findUnique({ + where: { + id: mealPlanId, + OR: [ + { + userId, + }, + { + collaboratorUsers: { + some: { + userId, + }, + }, + }, + ], + }, + select: { + id: true, + userId: true, + collaboratorUsers: { + select: { + userId: true, + }, + }, + }, + }); + + if (!mealPlan) { + return { + level: MealPlanAccessLevel.None, + ownerId: null, + collaboratorIds: null, + subscriberIds: null, + }; + } + + const ownerId = mealPlan.userId; + const collaboratorIds = mealPlan.collaboratorUsers.map((el) => el.userId); + const subscriberIds = [ownerId, ...collaboratorIds]; + + if (mealPlan.userId === userId) { + return { + level: MealPlanAccessLevel.Owner, + ownerId, + collaboratorIds, + subscriberIds, + }; + } + + return { + level: MealPlanAccessLevel.Collaborator, + ownerId, + collaboratorIds, + subscriberIds, + }; +}; diff --git a/packages/util/server/src/db/index.ts b/packages/util/server/src/db/index.ts index f2f626472..915fb5bc8 100644 --- a/packages/util/server/src/db/index.ts +++ b/packages/util/server/src/db/index.ts @@ -1,6 +1,8 @@ +export * from "./convertPrismaDateToDatestamp"; export * from "./getFriendshipIds"; export * from "./getFriendshipUsers"; export * from "./getRecipesWithConstraints"; export * from "./getSimilarRecipes"; export * from "./getVisibleProfileItems"; export * from "./getVisibleLabels"; +export * from "./getAccessToMealPlan"; diff --git a/packages/util/server/src/general/config.ts b/packages/util/server/src/general/config.ts index 45013f151..25658900d 100644 --- a/packages/util/server/src/general/config.ts +++ b/packages/util/server/src/general/config.ts @@ -6,18 +6,29 @@ enum Environment { All = "all", } -const getEnvString = (name: string, requiredEnvironments: Environment[]) => { +const getEnvString = < + T extends Exclude[] | Environment.All, + R extends T extends Environment.All ? string : string | undefined, +>( + name: string, + requiredEnvironments: T, +): R => { const value = process.env[name]; - const isRequired = - requiredEnvironments.includes( - (process.env.NODE_ENV || "production") as Environment, - ) || requiredEnvironments.includes(Environment.All); + let isRequired; + if (requiredEnvironments === Environment.All) { + isRequired = true; + } else { + const _requiredEnvironments = requiredEnvironments as Environment[]; + const nodeEnv = process.env.NODE_ENV || "production"; + isRequired = _requiredEnvironments.includes(nodeEnv as Environment); + } + if (!value && isRequired) { throw new Error(`Missing required environment variable: ${name}`); } - return value; + return value as R; }; export const config = { @@ -29,4 +40,8 @@ export const config = { ]), }, }, + grip: { + url: getEnvString("GRIP_URL", Environment.All), + key: getEnvString("GRIP_KEY", Environment.All), + }, }; diff --git a/packages/util/server/src/general/extendSession.ts b/packages/util/server/src/general/extendSession.ts new file mode 100644 index 000000000..3d3229751 --- /dev/null +++ b/packages/util/server/src/general/extendSession.ts @@ -0,0 +1,35 @@ +import { Session } from "@prisma/client"; +import { prisma } from "@recipesage/prisma"; +import * as Sentry from "@sentry/node"; + +const SESSION_VALIDITY_LENGTH = 30; // Initial session validity time +const RENEW_WHEN_EXPIRES_WITHIN_DAYS = 29; // If session expires within this many days, we extend the session + +export async function extendSession(session: Session) { + // We will not extend sessions that are already expired + if (session.expires < new Date()) { + return; + } + + // Extend the session expiry only if necessary + const graceDate = new Date(); + graceDate.setDate(graceDate.getDate() + RENEW_WHEN_EXPIRES_WITHIN_DAYS); + + if (session.expires < graceDate) { + const newExpiry = new Date(); + newExpiry.setDate(newExpiry.getDate() + SESSION_VALIDITY_LENGTH); + + prisma.session + .update({ + where: { + id: session.id, + }, + data: { + expires: newExpiry, + }, + }) + .catch((err) => { + Sentry.captureException(err); + }); + } +} diff --git a/packages/util/server/src/general/extractTextFromPDF.ts b/packages/util/server/src/general/extractTextFromPDF.ts new file mode 100644 index 000000000..6b06c53ea --- /dev/null +++ b/packages/util/server/src/general/extractTextFromPDF.ts @@ -0,0 +1,28 @@ +import { spawn } from "node:child_process"; + +const MAX_EXTRACT_TIME = 10000; + +export const extractTextFromPDF = async ( + source: Buffer, + maxPages?: number, +): Promise => { + return new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + reject("Timeout while waiting for pdftotext"); + }, MAX_EXTRACT_TIME); + + const proc = spawn("pdftotext", [ + "-", + "-", + "-f", + String(1), + "-l", + String(maxPages), + ]); + proc.stdout.on("data", function (data) { + clearTimeout(timeout); + resolve(String(data)); + }); + proc.stdin.end(source); + }); +}; diff --git a/packages/util/server/src/general/grip.ts b/packages/util/server/src/general/grip.ts new file mode 100644 index 000000000..15fbb457c --- /dev/null +++ b/packages/util/server/src/general/grip.ts @@ -0,0 +1,42 @@ +import { WebSocketMessageFormat } from "@fanoutio/grip"; +import { ServeGrip } from "@fanoutio/serve-grip"; +import { config } from "./config"; + +export const serveGrip = new ServeGrip({ + grip: [ + { + control_uri: config.grip.url, + key: config.grip.key, + }, + ], +}); + +export enum WSBoardcastEventType { + MealPlanUpdated = "mealplan:updated", +} + +export type WSBoardcastEventData = { + [WSBoardcastEventType.MealPlanUpdated]: { + reference: string; + mealPlanId: string; + }; +}; + +export const broadcastWSEvent = function ( + channel: string, + type: T, + data: WSBoardcastEventData[T], +) { + if (process.env.NODE_ENV === "test") return; + + const body = { + type: type, + data: data || {}, + }; + + const publisher = serveGrip.getPublisher(); + publisher.publishFormats( + channel, + new WebSocketMessageFormat(JSON.stringify(body)), + ); +}; diff --git a/packages/util/server/src/general/index.ts b/packages/util/server/src/general/index.ts index c4fabeb2f..ec53f334e 100644 --- a/packages/util/server/src/general/index.ts +++ b/packages/util/server/src/general/index.ts @@ -1,5 +1,8 @@ export * from "./sortRecipeImages"; export * from "./stripNumberedRecipeTitle"; +export * from "./validateTrpcSession"; export * from "./validateSession"; export * from "./generateSession"; +export * from "./extendSession"; export * from "./config"; +export * from "./grip"; diff --git a/packages/util/server/src/general/validateSession.ts b/packages/util/server/src/general/validateSession.ts index 81968ceee..b06c4849b 100644 --- a/packages/util/server/src/general/validateSession.ts +++ b/packages/util/server/src/general/validateSession.ts @@ -1,13 +1,14 @@ import { Session } from "@prisma/client"; -import { TRPCError } from "@trpc/server"; +import { prisma } from "@recipesage/prisma"; -export function validateSession( - session: Session | null, -): asserts session is Session { - if (!session) { - throw new TRPCError({ - message: "Must be logged in", - code: "UNAUTHORIZED", - }); - } +export async function validateSession( + token: string, +): Promise { + const session = await prisma.session.findFirst({ + where: { + token, + }, + }); + + return session || undefined; } diff --git a/packages/util/server/src/general/validateTrpcSession.ts b/packages/util/server/src/general/validateTrpcSession.ts new file mode 100644 index 000000000..e5582086d --- /dev/null +++ b/packages/util/server/src/general/validateTrpcSession.ts @@ -0,0 +1,14 @@ +import { Session } from "@prisma/client"; +import { TRPCError } from "@trpc/server"; + +// TODO: turn this into an authenticated procedure +export function validateTrpcSession( + session: Session | null, +): asserts session is Session { + if (!session) { + throw new TRPCError({ + message: "Must be logged in", + code: "UNAUTHORIZED", + }); + } +} diff --git a/packages/util/server/src/ml/pdfToRecipe.ts b/packages/util/server/src/ml/pdfToRecipe.ts index fbe707f3d..42765aac0 100644 --- a/packages/util/server/src/ml/pdfToRecipe.ts +++ b/packages/util/server/src/ml/pdfToRecipe.ts @@ -1,22 +1,10 @@ -import { extractTextFromPDF } from "../pdf/extractTextFromPDF"; -import { getImagesFromPDF } from "../pdf/getImagesFromPDF"; -import { ocrImageToRecipe } from "./ocrImageToRecipe"; +import { extractTextFromPDF } from "../general/extractTextFromPDF"; import { TextToRecipeInputType, textToRecipe } from "./textToRecipe"; -export const pdfToRecipe = async (pdf: Uint8Array, maxPages?: number) => { +export const pdfToRecipe = async (pdf: Buffer, maxPages?: number) => { const text = await extractTextFromPDF(pdf, maxPages); - if (!text) { - // Many PDFs don't actually contain text, but rather are just an image - // We fall back here to grabbing an image from the PDF, then OCRing that - // We only grab first page since OCR to Recipe only supports a single image currently - const images = await getImagesFromPDF(pdf, 1); - if (!images.length) return; - - const recipe = await ocrImageToRecipe(images[0]); - return recipe; - } - const recipe = await textToRecipe(text, TextToRecipeInputType.Document); + return recipe; }; diff --git a/packages/util/server/src/pdf/extractTextFromPDF.ts b/packages/util/server/src/pdf/extractTextFromPDF.ts deleted file mode 100644 index 062a34b18..000000000 --- a/packages/util/server/src/pdf/extractTextFromPDF.ts +++ /dev/null @@ -1,32 +0,0 @@ -const PDFJSPromise = import("pdfjs-dist"); -import { PDFDocumentProxy } from "pdfjs-dist"; -import type { - DocumentInitParameters, - TextItem, - TypedArray, -} from "pdfjs-dist/types/src/display/api"; - -const getPageText = async (pdf: PDFDocumentProxy, pageNum: number) => { - const page = await pdf.getPage(pageNum); - const tokenizedText = await page.getTextContent(); - const pageText = tokenizedText.items - .filter((token): token is TextItem => "str" in token) - .map((token) => token.str) - .join(""); - return pageText; -}; - -export const extractTextFromPDF = async ( - source: string | URL | TypedArray | ArrayBuffer | DocumentInitParameters, - maxPages?: number, -): Promise => { - const PDFJS = await PDFJSPromise; - const pdf: PDFDocumentProxy = await PDFJS.getDocument(source).promise; - maxPages ||= pdf.numPages; - const pageTextPromises = []; - for (let pageNum = 1; pageNum <= maxPages; pageNum += 1) { - pageTextPromises.push(getPageText(pdf, pageNum)); - } - const pageTexts = await Promise.all(pageTextPromises); - return pageTexts.join(" "); -}; diff --git a/packages/util/server/src/pdf/getImagesFromPDF.ts b/packages/util/server/src/pdf/getImagesFromPDF.ts deleted file mode 100644 index 8d758e030..000000000 --- a/packages/util/server/src/pdf/getImagesFromPDF.ts +++ /dev/null @@ -1,47 +0,0 @@ -const PDFJSPromise = import("pdfjs-dist"); -import { PDFDocumentProxy } from "pdfjs-dist"; -import type { - DocumentInitParameters, - TypedArray, -} from "pdfjs-dist/types/src/display/api"; -import * as Canvas from "canvas"; - -const getPageImage = async (pdf: PDFDocumentProxy, pageNum: number) => { - const page = await pdf.getPage(pageNum); - const viewport = page.getViewport({ - scale: 2, - rotation: 0, - dontFlip: false, - }); - console.log(viewport.width, viewport.height); - const canvas = Canvas.createCanvas( - viewport.width || 1024, - viewport.height || 1024, - ); - const context = canvas.getContext("2d"); - - await page.render({ - canvasContext: context as unknown as CanvasRenderingContext2D, - viewport: viewport, - }).promise; - - const image = canvas.toBuffer("image/jpeg"); - - return image; -}; - -export const getImagesFromPDF = async ( - source: string | URL | TypedArray | ArrayBuffer | DocumentInitParameters, - maxPages?: number, -): Promise => { - const PDFJS = await PDFJSPromise; - const pdf: PDFDocumentProxy = await PDFJS.getDocument(source).promise; - maxPages ||= pdf.numPages; - const pageImagePromises = []; - for (let pageNum = 1; pageNum <= maxPages; pageNum += 1) { - pageImagePromises.push(getPageImage(pdf, pageNum)); - } - const pageImages = await Promise.all(pageImagePromises); - - return pageImages; -}; diff --git a/packages/util/server/src/pdf/index.ts b/packages/util/server/src/pdf/index.ts deleted file mode 100644 index b73586ecb..000000000 --- a/packages/util/server/src/pdf/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./extractTextFromPDF"; -export * from "./getImagesFromPDF"; diff --git a/packages/util/server/src/search/typesense.ts b/packages/util/server/src/search/typesense.ts index d00c61924..8d5540519 100644 --- a/packages/util/server/src/search/typesense.ts +++ b/packages/util/server/src/search/typesense.ts @@ -14,7 +14,14 @@ if (process.env.SEARCH_PROVIDER === "typesense") { connectionTimeoutSeconds: 10, }); - init(); + init() + .then(() => { + console.log("Typesense initialized"); + }) + .catch((e) => { + console.error("Typesense init failure"); + console.error(e); + }); } async function init() { diff --git a/tsconfig.base.json b/tsconfig.base.json index c24cf3d75..9e4f2d3fd 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -31,7 +31,6 @@ "packages/util/server/src/general/index.ts" ], "@recipesage/util/server/ml": ["packages/util/server/src/ml/index.ts"], - "@recipesage/util/server/pdf": ["packages/util/server/src/pdf/index.ts"], "@recipesage/util/server/search": [ "packages/util/server/src/search/index.ts" ],