diff --git a/package-lock.json b/package-lock.json index 639517a..344b38e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,16 +8,14 @@ "name": "doof", "version": "0.1.0", "dependencies": { - "core-js": "^3.8.3", "vue": "^3.5.12", "vue-router": "^4.4.5" }, "devDependencies": { - "@babel/core": "^7.12.16", - "@babel/eslint-parser": "^7.12.16", "@types/node": "^20.19.22", "@typescript-eslint/eslint-plugin": "^7.18.0", "@typescript-eslint/parser": "^7.18.0", + "@vitest/coverage-v8": "^1.6.0", "@vue/cli-plugin-babel": "~5.0.0", "@vue/cli-plugin-eslint": "~5.0.0", "@vue/cli-plugin-typescript": "~5.0.0", @@ -27,12 +25,10 @@ "husky": "^8.0.0", "lint-staged": "^13.3.0", "msw": "^2.5.2", - "node-fetch": "^2.6.9", "openapi-fetch": "^0.9.5", "openapi-typescript": "^7.4.2", "prettier": "^3.3.3", "typescript": "~5.5.4", - "undici": "^6.19.8", "vitest": "^1.6.0", "vue-tsc": "^2.0.29" } @@ -127,24 +123,6 @@ "url": "https://opencollective.com/babel" } }, - "node_modules/@babel/eslint-parser": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.23.3.tgz", - "integrity": "sha512-9bTuNlyx7oSstodm1cR1bECj4fkiknsDa1YniISkJemMY3DGhJNYBECbe6QD/q54mp2J8VO66jW3/7uP//iFCw==", - "dev": true, - "dependencies": { - "@nicolo-ribaudo/eslint-scope-5-internals": "5.1.1-v1", - "eslint-visitor-keys": "^2.1.0", - "semver": "^6.3.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || >=14.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.11.0", - "eslint": "^7.5.0 || ^8.0.0" - } - }, "node_modules/@babel/generator": { "version": "7.23.6", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.6.tgz", @@ -1821,6 +1799,13 @@ "node": ">=6.9.0" } }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT" + }, "node_modules/@discoveryjs/json-ext": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", @@ -2531,6 +2516,16 @@ } } }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/@jest/schemas": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", @@ -2593,10 +2588,11 @@ "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.20", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz", - "integrity": "sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==", + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", "dev": true, + "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" @@ -2626,15 +2622,6 @@ "node": ">=18" } }, - "node_modules/@nicolo-ribaudo/eslint-scope-5-internals": { - "version": "5.1.1-v1", - "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz", - "integrity": "sha512-54/JRvkLIzzDWshCWfuhadfrfZVPiElY8Fcgmg1HroEly/EDSszzhBAsarCux+D/kOslTRquNzuyGSmUSTTHGg==", - "dev": true, - "dependencies": { - "eslint-scope": "5.1.1" - } - }, "node_modules/@node-ipc/js-queue": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@node-ipc/js-queue/-/js-queue-2.0.3.tgz", @@ -3723,6 +3710,34 @@ "dev": true, "license": "ISC" }, + "node_modules/@vitest/coverage-v8": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-1.6.1.tgz", + "integrity": "sha512-6YeRZwuO4oTGKxD3bijok756oktHSIm3eczVVzNe3scqzuhLwltIF3S9ZL/vwOVIpURmU6SnZhziXXAfw8/Qlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.2.1", + "@bcoe/v8-coverage": "^0.2.3", + "debug": "^4.3.4", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-lib-source-maps": "^5.0.4", + "istanbul-reports": "^3.1.6", + "magic-string": "^0.30.5", + "magicast": "^0.3.3", + "picocolors": "^1.0.0", + "std-env": "^3.5.0", + "strip-literal": "^2.0.0", + "test-exclude": "^6.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "vitest": "1.6.1" + } + }, "node_modules/@vitest/expect": { "version": "1.6.1", "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.6.1.tgz", @@ -6249,6 +6264,7 @@ "version": "3.35.0", "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.35.0.tgz", "integrity": "sha512-ntakECeqg81KqMueeGJ79Q5ZgQNR+6eaE8sxGCx62zMbAIj65q+uYvatToew3m6eAGdU4gNZwpZ34NMe4GYswg==", + "dev": true, "hasInstallScript": true, "funding": { "type": "opencollective", @@ -7350,15 +7366,6 @@ "node": ">=8.0.0" } }, - "node_modules/eslint-visitor-keys": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", - "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", - "dev": true, - "engines": { - "node": ">=10" - } - }, "node_modules/eslint-webpack-plugin": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/eslint-webpack-plugin/-/eslint-webpack-plugin-3.2.0.tgz", @@ -9294,6 +9301,112 @@ "node": ">=0.10.0" } }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report/node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/istanbul-lib-report/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report/node_modules/supports-color": { + "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, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.23", + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/javascript-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/javascript-stringify/-/javascript-stringify-2.1.0.tgz", @@ -10366,6 +10479,18 @@ "@jridgewell/sourcemap-codec": "^1.5.0" } }, + "node_modules/magicast": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz", + "integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.25.4", + "@babel/types": "^7.25.4", + "source-map-js": "^1.2.0" + } + }, "node_modules/make-dir": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", @@ -13516,6 +13641,21 @@ "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "dev": true }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -13912,16 +14052,6 @@ "dev": true, "license": "MIT" }, - "node_modules/undici": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.22.0.tgz", - "integrity": "sha512-hU/10obOIu62MGYjdskASR3CUAiYaFTtC9Pa6vHyf//mAipSvSQg6od2CnJswq7fvzNS3zJhxoRkgNVaHurWKw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18.17" - } - }, "node_modules/undici-types": { "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", @@ -15515,17 +15645,6 @@ "semver": "^6.3.1" } }, - "@babel/eslint-parser": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.23.3.tgz", - "integrity": "sha512-9bTuNlyx7oSstodm1cR1bECj4fkiknsDa1YniISkJemMY3DGhJNYBECbe6QD/q54mp2J8VO66jW3/7uP//iFCw==", - "dev": true, - "requires": { - "@nicolo-ribaudo/eslint-scope-5-internals": "5.1.1-v1", - "eslint-visitor-keys": "^2.1.0", - "semver": "^6.3.1" - } - }, "@babel/generator": { "version": "7.23.6", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.6.tgz", @@ -16667,6 +16786,12 @@ "to-fast-properties": "^2.0.0" } }, + "@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true + }, "@discoveryjs/json-ext": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", @@ -17023,6 +17148,12 @@ "dev": true, "requires": {} }, + "@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true + }, "@jest/schemas": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", @@ -17071,9 +17202,9 @@ "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==" }, "@jridgewell/trace-mapping": { - "version": "0.3.20", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz", - "integrity": "sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==", + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", "dev": true, "requires": { "@jridgewell/resolve-uri": "^3.1.0", @@ -17100,15 +17231,6 @@ "strict-event-emitter": "^0.5.1" } }, - "@nicolo-ribaudo/eslint-scope-5-internals": { - "version": "5.1.1-v1", - "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz", - "integrity": "sha512-54/JRvkLIzzDWshCWfuhadfrfZVPiElY8Fcgmg1HroEly/EDSszzhBAsarCux+D/kOslTRquNzuyGSmUSTTHGg==", - "dev": true, - "requires": { - "eslint-scope": "5.1.1" - } - }, "@node-ipc/js-queue": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@node-ipc/js-queue/-/js-queue-2.0.3.tgz", @@ -17868,6 +17990,27 @@ "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", "dev": true }, + "@vitest/coverage-v8": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-1.6.1.tgz", + "integrity": "sha512-6YeRZwuO4oTGKxD3bijok756oktHSIm3eczVVzNe3scqzuhLwltIF3S9ZL/vwOVIpURmU6SnZhziXXAfw8/Qlw==", + "dev": true, + "requires": { + "@ampproject/remapping": "^2.2.1", + "@bcoe/v8-coverage": "^0.2.3", + "debug": "^4.3.4", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-lib-source-maps": "^5.0.4", + "istanbul-reports": "^3.1.6", + "magic-string": "^0.30.5", + "magicast": "^0.3.3", + "picocolors": "^1.0.0", + "std-env": "^3.5.0", + "strip-literal": "^2.0.0", + "test-exclude": "^6.0.0" + } + }, "@vitest/expect": { "version": "1.6.1", "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.6.1.tgz", @@ -19755,7 +19898,8 @@ "core-js": { "version": "3.35.0", "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.35.0.tgz", - "integrity": "sha512-ntakECeqg81KqMueeGJ79Q5ZgQNR+6eaE8sxGCx62zMbAIj65q+uYvatToew3m6eAGdU4gNZwpZ34NMe4GYswg==" + "integrity": "sha512-ntakECeqg81KqMueeGJ79Q5ZgQNR+6eaE8sxGCx62zMbAIj65q+uYvatToew3m6eAGdU4gNZwpZ34NMe4GYswg==", + "dev": true }, "core-js-compat": { "version": "3.35.0", @@ -20733,12 +20877,6 @@ "estraverse": "^4.1.1" } }, - "eslint-visitor-keys": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", - "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", - "dev": true - }, "eslint-webpack-plugin": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/eslint-webpack-plugin/-/eslint-webpack-plugin-3.2.0.tgz", @@ -21907,6 +22045,76 @@ "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", "dev": true }, + "istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true + }, + "istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "requires": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "dependencies": { + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "requires": { + "semver": "^7.5.3" + } + }, + "semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true + }, + "supports-color": { + "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, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "istanbul-lib-source-maps": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", + "dev": true, + "requires": { + "@jridgewell/trace-mapping": "^0.3.23", + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0" + } + }, + "istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "requires": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + } + }, "javascript-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/javascript-stringify/-/javascript-stringify-2.1.0.tgz", @@ -22660,6 +22868,17 @@ "@jridgewell/sourcemap-codec": "^1.5.0" } }, + "magicast": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz", + "integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==", + "dev": true, + "requires": { + "@babel/parser": "^7.25.4", + "@babel/types": "^7.25.4", + "source-map-js": "^1.2.0" + } + }, "make-dir": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", @@ -24939,6 +25158,17 @@ } } }, + "test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "requires": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + } + }, "text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -25212,12 +25442,6 @@ "integrity": "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==", "dev": true }, - "undici": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.22.0.tgz", - "integrity": "sha512-hU/10obOIu62MGYjdskASR3CUAiYaFTtC9Pa6vHyf//mAipSvSQg6od2CnJswq7fvzNS3zJhxoRkgNVaHurWKw==", - "dev": true - }, "undici-types": { "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", diff --git a/package.json b/package.json index 9876c52..c330366 100644 --- a/package.json +++ b/package.json @@ -10,39 +10,35 @@ "prepare": "husky install", "test": "vitest run", "test:watch": "vitest", - "test:coverage": "vitest run --coverage", - "typecheck": "tsc -p tsconfig.json --noEmit", - "typecheck:vue": "vue-tsc --noEmit", + + "typecheck": "tsc -p tsconfig.json --noEmit", + "typecheck:vue": "vue-tsc --noEmit", "codegen:api": "openapi-typescript ../munch-ease-backend/openapi.json -o src/api/types.ts", "codegen": "npm run codegen:api", "codegen:check": "node scripts/codegen-check.js" }, "dependencies": { - "core-js": "^3.8.3", "vue": "^3.5.12", "vue-router": "^4.4.5" }, "devDependencies": { - "@babel/core": "^7.12.16", - "@babel/eslint-parser": "^7.12.16", "@types/node": "^20.19.22", - "@typescript-eslint/eslint-plugin": "^7.18.0", - "@typescript-eslint/parser": "^7.18.0", + "@typescript-eslint/eslint-plugin": "^7.18.0", + "@typescript-eslint/parser": "^7.18.0", + "@vue/cli-plugin-babel": "~5.0.0", "@vue/cli-plugin-eslint": "~5.0.0", "@vue/cli-plugin-typescript": "~5.0.0", "@vue/cli-service": "~5.0.0", - "eslint": "^8.57.0", - "eslint-plugin-vue": "^9.27.0", + "eslint": "^8.57.0", + "eslint-plugin-vue": "^9.27.0", "husky": "^8.0.0", "lint-staged": "^13.3.0", "msw": "^2.5.2", - "node-fetch": "^2.6.9", "openapi-fetch": "^0.9.5", "openapi-typescript": "^7.4.2", "prettier": "^3.3.3", - "typescript": "~5.5.4", - "undici": "^6.19.8", + "typescript": "~5.5.4", "vitest": "^1.6.0", "vue-tsc": "^2.0.29" }, @@ -80,10 +76,19 @@ }, "overrides": [ { - "files": ["src/**/*.ts", "src/**/*.vue", "src/**/*.d.ts"], - "excludedFiles": ["src/api/types.ts", "src/**/*.d.ts"], + "files": [ + "src/**/*.ts", + "src/**/*.vue", + "src/**/*.d.ts" + ], + "excludedFiles": [ + "src/api/types.ts", + "src/**/*.d.ts" + ], "parserOptions": { - "project": ["./tsconfig.eslint.json"], + "project": [ + "./tsconfig.eslint.json" + ], "tsconfigRootDir": "." }, "rules": { diff --git a/src/api/sdk.ts b/src/api/sdk.ts index c6eca08..ca1c644 100644 --- a/src/api/sdk.ts +++ b/src/api/sdk.ts @@ -1,6 +1,6 @@ import { api } from '@/api/client' import type { components } from '@/api/types' -import { toDate, decodeMeal, decodeRecipe, decodeIngredients, decodeShoppingList, decodeShoppingListItems, decodeIngredient } from '@/domain/decoders' +import { toDate, decodeMeal, decodeRecipe, decodeIngredients, decodeShoppingList, decodeShoppingListItems, decodeIngredient, decodeLookup } from '@/domain/decoders' import type { Recipe, Meal, @@ -19,22 +19,7 @@ function httpError(response: Response, error: unknown): Error { return new Error(`${response.status} ${response.statusText || 'HTTP error'}`) } -// Small helper to decode optional lookup maps without repeating loops everywhere -function decodeLookup( - raw: Record | null | undefined, - decode: (v: TIn) => TOut | null -): Record | undefined { - if (!raw) return undefined - const out: Record = {} - for (const key of Object.keys(raw)) { - if (!Object.prototype.hasOwnProperty.call(raw, key)) continue - const maybe = raw[key] - if (maybe === undefined) continue - const decoded = decode(maybe) - if (decoded) out[String(key)] = decoded - } - return out -} +// decodeLookup moved to domain/decoders to be reused across SDK and other modules // Shopping list mapped view types now come from domain/types @@ -50,17 +35,21 @@ function attachItemRefs( const listId = item.listId ?? undefined const created = item.createdDate - if (ingredientId !== undefined && lookups.ingredientsLookup && lookups.ingredientsLookup[String(ingredientId)] !== undefined) { - item.ingredient = lookups.ingredientsLookup[String(ingredientId)] + if (ingredientId !== undefined && lookups.ingredientsLookup) { + const v = lookups.ingredientsLookup[String(ingredientId)] + if (v !== undefined) item.ingredient = v } - if (mealId !== undefined && lookups.mealsLookup && lookups.mealsLookup[String(mealId)] !== undefined) { - item.meal = lookups.mealsLookup[String(mealId)] + if (mealId !== undefined && lookups.mealsLookup) { + const v = lookups.mealsLookup[String(mealId)] + if (v !== undefined) item.meal = v } - if (recipeId !== undefined && lookups.recipesLookup && lookups.recipesLookup[String(recipeId)] !== undefined) { - item.recipe = lookups.recipesLookup[String(recipeId)] + if (recipeId !== undefined && lookups.recipesLookup) { + const v = lookups.recipesLookup[String(recipeId)] + if (v !== undefined) item.recipe = v } - if (listId !== undefined && lookups.shoppingListLookup && lookups.shoppingListLookup[String(listId)] !== undefined) { - item.list = lookups.shoppingListLookup[String(listId)] + if (listId !== undefined && lookups.shoppingListLookup) { + const v = lookups.shoppingListLookup[String(listId)] + if (v !== undefined) item.list = v } if (created !== undefined) item.createdDate = toDate(created) } @@ -88,7 +77,7 @@ export function mapPurchasedShoppingList(dto: components['schemas']['PurchasedSh ...(ingredientsLookup && { ingredientsLookup }), ...(mealsLookup && { mealsLookup }), ...(recipesLookup && { recipesLookup }), - shoppingListLookup: { [String(list.id)]: list }, + ...(list && { shoppingListLookup: { [String(list.id)]: list } }), } attachItemRefs(list.items, lookups) } @@ -99,6 +88,8 @@ export function mapPurchasedShoppingList(dto: components['schemas']['PurchasedSh ...(mealsLookup && { mealsLookup }), ...(recipesLookup && { recipesLookup }), ...(list && { list }), + // include shoppingListLookup when list exists for consistency with lookups type + ...(list && { shoppingListLookup: { [String(list.id)]: list } }), } } @@ -155,14 +146,15 @@ export async function listRecipes(params?: { q?: string | null; cursor?: string } const { data, error, response } = await api.GET('/api/v1/recipes', { params: { query } }) if (!response.ok) throw httpError(response, error) - const mapped = fromOpenApiPage(data ?? null, (r) => decodeRecipe(r)) - return { ...mapped, items: mapped.items.filter((r): r is Recipe => !!r) } + return fromOpenApiPage(data ?? null, (r) => decodeRecipe(r)) } -export async function getRecipe(id: number | string): Promise { +export async function getRecipe(id: number | string): Promise { const { data, error, response } = await api.GET('/api/v1/recipes/{recipe_id}', { params: { path: { recipe_id: Number(id) } } }) if (!response.ok) throw httpError(response, error) - return decodeRecipe(data) + const mapped = decodeRecipe(data) + if (!mapped) throw new Error('Recipe not found') + return mapped } export async function saveRecipe(recipe: components['schemas']['Recipe-Input']): Promise { @@ -201,7 +193,7 @@ export async function parseProduct( } // Persons -export async function listPersons(params?: { q?: string | null; cursor?: string | null; limit?: number }): Promise> { +async function listPersons(params?: { q?: string | null; cursor?: string | null; limit?: number }): Promise> { const query: Record = {} if (params) { if (params.q !== undefined) query.q = params.q @@ -235,10 +227,12 @@ export async function getUpcomingMeals(from: Date, to: Date): Promise { .sort((a, b) => ((a.suggestedDate?.getTime() ?? 0) - (b.suggestedDate?.getTime() ?? 0))) } -export async function getMeal(id: number | string): Promise { +export async function getMeal(id: number | string): Promise { const { data, error, response } = await api.GET('/api/v1/meals/{meal_id}', { params: { path: { meal_id: Number(id) } } }) if (!response.ok) throw httpError(response, error) - return decodeMeal(data) + const mapped = decodeMeal(data) + if (!mapped) throw new Error('Meal not found') + return mapped } export async function saveMeal(meal: components['schemas']['Meal-Input']): Promise { @@ -257,12 +251,14 @@ export async function saveMeal(meal: components['schemas']['Meal-Input']): Promi } } -export async function markMealConsumed(mealId: number | string): Promise { +export async function markMealConsumed(mealId: number | string): Promise { const { data, error, response } = await api.POST('/api/v1/meals/{meal_id}/consumed', { params: { path: { meal_id: Number(mealId) }, cookie: { user_id: 0 } }, }) if (!response.ok) throw httpError(response, error) - return decodeMeal(data) + const mapped = decodeMeal(data) + if (!mapped) throw new Error('Meal not found') + return mapped } export async function deleteMeal(mealId: number | string): Promise { @@ -288,24 +284,27 @@ export async function saveMyShoppingList(items: components['schemas']['Ingredien return decodeIngredients(data ?? []) } -export async function getShoppingList(id: number | string) { +export async function getShoppingList(id: number | string): Promise { const { data, error, response } = await api.GET('/api/v1/shopping/{list_id}', { params: { path: { list_id: Number(id) } } }) if (!response.ok) throw httpError(response, error) const mapped = mapPurchasedShoppingList(data) return mapped?.list ?? null } -export async function getCurrentShoppingList() { +export async function getCurrentShoppingList(): Promise { const { data, error, response } = await api.GET('/api/v1/shopping/current') if (!response.ok) throw httpError(response, error) - return mapCurrentShoppingList(data) + const mapped = mapCurrentShoppingList(data) + if (!mapped) throw new Error('Failed to map current shopping list') + return mapped } type PurchaseExisting = { type: 'existing'; id: number; personId: number; ingredientId?: number | null } type PurchaseRefs = { type: 'refs'; personId: number; ingredientId?: number | null; recipeId?: number | null; mealId?: number | null } +export type PurchaseRequest = PurchaseExisting | PurchaseRefs export async function purchaseShoppingList( - completedRequests: Array + completedRequests: PurchaseRequest[] ): Promise { if (!Array.isArray(completedRequests) || completedRequests.length === 0) return null // Map incoming requests: if id provided and >= 0, use it; otherwise send identifiers for ingredient/recipe/meal diff --git a/src/api/types.ts b/src/api/types.ts index 9f4eb31..b49e475 100644 --- a/src/api/types.ts +++ b/src/api/types.ts @@ -506,9 +506,10 @@ export interface components { prevCursor?: string | null; /** * Total - * @description Optional total count + * @description Total count + * @default 0 */ - total?: number | null; + total: number; }; /** Page[Recipe] */ Page_Recipe_: { @@ -520,9 +521,10 @@ export interface components { prevCursor?: string | null; /** * Total - * @description Optional total count + * @description Total count + * @default 0 */ - total?: number | null; + total: number; }; /** Person */ Person: { @@ -575,8 +577,6 @@ export interface components { imgSmall: string; /** Imglarge */ imgLarge: string; - /** Rawdata */ - rawData?: Record | null; }; /** ProductUrl */ ProductUrl: { @@ -626,7 +626,7 @@ export interface components { */ dateCreated?: string; /** Createdbyid */ - createdById: number | null; + createdById: number; createdBy?: components["schemas"]["Person"] | null; /** Datehidden */ dateHidden?: string | null; @@ -659,7 +659,7 @@ export interface components { */ dateCreated?: string; /** Createdbyid */ - createdById: number | null; + createdById: number; createdBy?: components["schemas"]["Person"] | null; /** Datehidden */ dateHidden?: string | null; diff --git a/src/components/meals/EditMealPage.vue b/src/components/meals/EditMealPage.vue index 5fcd93e..1aa98ab 100644 --- a/src/components/meals/EditMealPage.vue +++ b/src/components/meals/EditMealPage.vue @@ -118,8 +118,8 @@ diff --git a/src/components/shopping/ShoppingListItem.vue b/src/components/shopping/ShoppingListItem.vue index 0d2eeb9..2a23df6 100644 --- a/src/components/shopping/ShoppingListItem.vue +++ b/src/components/shopping/ShoppingListItem.vue @@ -37,11 +37,11 @@ :key="source.id" > , and - - {{ formatQuantity(source.ingredient.quantity) }} {{ source.ingredient.unit }} for - {{ source.person.name }} + + + {{ formatQuantity(source.ingredient.quantity) }} {{ source.ingredient.unit }} - + {{ formatQuantity(source.ingredient.quantity) }} {{ source.ingredient.unit }} in {{ source.recipe.name @@ -57,7 +57,7 @@ : '' }} - + {{ source.ingredient.line }} for {{ source.meal?.suggestedDate @@ -78,11 +78,11 @@ :key="source.id" > , and - - {{ formatQuantity(source.ingredient.quantity) }} {{ source.ingredient.unit }} for - {{ source.person.name }} + + + {{ formatQuantity(source.ingredient.quantity) }} {{ source.ingredient.unit }} - + {{ formatQuantity(source.ingredient.quantity) }} {{ source.ingredient.unit }} in {{ source.recipe.name @@ -98,7 +98,7 @@ : '' }} - + {{ source.ingredient.line }} for {{ source.meal.suggestedDate @@ -134,10 +134,13 @@ const imageSrc = computed(() => { const remainingRequiredTotals = computed(() => calculateTotals( - props.shoppingListItemGroup.shoppingListItems.map((item) => ({ - quantity: item.ingredient.quantity, - unit: String(item.ingredient.unit || 'items'), - })) + props.shoppingListItemGroup.shoppingListItems + .filter((item) => !!item.ingredient) + .map((item) => ({ + // item.ingredient is defined due to filter above + quantity: item.ingredient!.quantity, + unit: String(item.ingredient!.unit || 'items'), + })) ) ) diff --git a/src/composables/useMeals.ts b/src/composables/useMeals.ts deleted file mode 100644 index 3b9c51f..0000000 --- a/src/composables/useMeals.ts +++ /dev/null @@ -1,46 +0,0 @@ -import * as sdk from '@/api/sdk' -import type { MealInput, Meal } from '@/domain/types' - -export async function getUpcomingMeals(from: Date, to: Date) { - return sdk.getUpcomingMeals(from, to) -} - -export async function getMeal(id: number | string) { - return sdk.getMeal(id) -} - -export async function saveMeal(meal: MealInput) { - return sdk.saveMeal(meal) -} - -export async function markMealConsumed(mealId: number | string) { - return sdk.markMealConsumed(mealId) -} - -export async function deleteMeal(mealId: number | string) { - return sdk.deleteMeal(mealId) -} - -// Helper to convert domain Meal to MealInput, keeping Date→string conversion in boundary -export function toMealInput(meal: Meal): MealInput { - return { - id: meal.id, - suggestedDate: meal.suggestedDate ? meal.suggestedDate.toISOString() : new Date().toISOString(), - consumedDate: meal.consumedDate ? meal.consumedDate.toISOString() : null, - purchaseDate: meal.purchaseDate ? meal.purchaseDate.toISOString() : null, - chefs: meal.chefs, - cleanup: meal.cleanup, - consumers: meal.consumers, - recipes: meal.recipes.map((r) => ({ - mealId: r.mealId, - recipeId: r.recipeId, - servings: r.servings, - recipe: null - })), - extraIngredients: meal.extraIngredients, - } -} - -export function useMeals() { - return { getUpcomingMeals, getMeal, saveMeal, markMealConsumed, deleteMeal, toMealInput } -} diff --git a/src/composables/useShopping.ts b/src/composables/useShopping.ts index b70d0df..6c59922 100644 --- a/src/composables/useShopping.ts +++ b/src/composables/useShopping.ts @@ -1,27 +1,15 @@ import * as sdk from '@/api/sdk' -import type { ShoppingListItemWithRefs, Product, Meal, Ingredient, Person, Recipe } from '@/domain/types' +import type { ShoppingListItemWithRefs, Product, Meal } from '@/domain/types' -export type UIShoppingListItem = { - id: number - ingredient: Ingredient - person?: Person | null - personId: number - recipe?: Recipe | null - meal?: Meal | null - ingredientId?: number | null - listId?: number | null - createdDate?: Date | null -} - -export type GroupByProduct = { type: 'product'; product: Product; shoppingListItems: UIShoppingListItem[] } -export type GroupByName = { type: 'name'; name: string; shoppingListItems: UIShoppingListItem[] } +export type GroupByProduct = { type: 'product'; product: Product; shoppingListItems: ShoppingListItemWithRefs[] } +export type GroupByName = { type: 'name'; name: string; shoppingListItems: ShoppingListItemWithRefs[] } export type Group = GroupByProduct | GroupByName -export function groupsToItems(groups: Group[]): UIShoppingListItem[] { +export function groupsToItems(groups: Group[]): ShoppingListItemWithRefs[] { return groups.map((g) => g.shoppingListItems).flat() } -export function uniqueMeals(shoppingListItems: UIShoppingListItem[]): Meal[] { +export function uniqueMeals(shoppingListItems: ShoppingListItemWithRefs[]): Meal[] { const mealsWithDuplicates = shoppingListItems.map((item) => item.meal).filter((m): m is Meal => !!m) const mealsLookup: Record = mealsWithDuplicates.reduce>( (acc, meal) => { @@ -33,11 +21,11 @@ export function uniqueMeals(shoppingListItems: UIShoppingListItem[]): Meal[] { return Object.values(mealsLookup) } -export function itemsToGroups(shoppingListItems: UIShoppingListItem[]): Group[] { +export function itemsToGroups(shoppingListItems: ShoppingListItemWithRefs[]): Group[] { const ingredients_by_product_id: Record = {} const ingredients_by_name: Record = {} for (const item of shoppingListItems) { - if (item.ingredient.product) { + if (item.ingredient?.product) { let group = ingredients_by_product_id[item.ingredient.product.id] if (!group) { group = ingredients_by_product_id[item.ingredient.product.id] = { @@ -48,11 +36,12 @@ export function itemsToGroups(shoppingListItems: UIShoppingListItem[]): Group[] } group.shoppingListItems.push(item) } else { - let group = ingredients_by_name[item.ingredient.name] + const name = item.ingredient?.name ?? '' + let group = ingredients_by_name[name] if (!group) { - group = ingredients_by_name[item.ingredient.name] = { + group = ingredients_by_name[name] = { type: 'name', - name: item.ingredient.name, + name, shoppingListItems: [], } } @@ -63,34 +52,8 @@ export function itemsToGroups(shoppingListItems: UIShoppingListItem[]): Group[] } export function useShopping() { - // Request shapes expected by sdk.purchaseShoppingList - type PurchaseExisting = { type: 'existing'; id: number; personId: number; ingredientId?: number | null } - type PurchaseRefs = { type: 'refs'; personId: number; ingredientId?: number | null; recipeId?: number | null; mealId?: number | null } - const toExisting = (id: number, personId: number, ingredientId?: number | null): PurchaseExisting => ({ type: 'existing', id, personId, ingredientId: ingredientId ?? null }) - const toRefs = (personId: number, ingredientId?: number | null, recipeId?: number | null, mealId?: number | null): PurchaseRefs => ({ - type: 'refs', - personId, - ingredientId: ingredientId ?? null, - recipeId: recipeId ?? null, - mealId: mealId ?? null, - }) - const mapItemToUI = (i: ShoppingListItemWithRefs): UIShoppingListItem => ({ - id: Number(i.id), - ingredient: i.ingredient!, - person: null, - personId: i.personId, - recipe: i.recipe ?? null, - meal: i.meal ?? null, - ingredientId: i.ingredient?.id ?? null, - listId: i.listId ?? null, - createdDate: i.createdDate, - }) - const groupsFrom = (items?: ShoppingListItemWithRefs[]): Group[] => { - return itemsToGroups((items ?? []).map(mapItemToUI)) - } - const mealsFrom = (items?: ShoppingListItemWithRefs[]): Meal[] => { - return uniqueMeals((items ?? []).map(mapItemToUI)) - } + const groupsFrom = (items?: ShoppingListItemWithRefs[]): Group[] => itemsToGroups(items ?? []) + const mealsFrom = (items?: ShoppingListItemWithRefs[]): Meal[] => uniqueMeals(items ?? []) return { getCurrentShoppingList: sdk.getCurrentShoppingList, getShoppingList: sdk.getShoppingList, @@ -100,15 +63,21 @@ export function useShopping() { getMyShoppingList: sdk.getMyShoppingList, saveMyShoppingList: sdk.saveMyShoppingList, // View-model helpers - mapItemToUI, groupsFrom, mealsFrom, async purchaseFromGroups(groups: Group[]) { - const items = groupsToItems(groups).map((i) => - typeof i.id === 'number' && i.id >= 0 - ? toExisting(i.id, i.personId, i.ingredientId) - : toRefs(i.personId, i.ingredient?.id ?? null, i.recipe?.id ?? null, i.meal?.id ?? null) - ) + const items = groupsToItems(groups).map((i): sdk.PurchaseRequest => { + if (typeof i.id === 'number' && i.id >= 0) { + return { type: 'existing', id: i.id, personId: i.personId, ingredientId: i.ingredient?.id ?? null } + } + return { + type: 'refs', + personId: i.personId, + ingredientId: i.ingredient?.id ?? null, + recipeId: i.recipe?.id ?? null, + mealId: i.meal?.id ?? null, + } + }) if (!items?.length) return null return sdk.purchaseShoppingList(items) }, diff --git a/src/domain/decoders.ts b/src/domain/decoders.ts index 055a53e..a3ac419 100644 --- a/src/domain/decoders.ts +++ b/src/domain/decoders.ts @@ -1,4 +1,4 @@ -import type { RecipeOut, Recipe, MealOut, Meal, MealRecipe, Ingredient as DomainIngredient, ShoppingList, ShoppingListItem } from './types' +import type { RecipeOut, Recipe, MealOut, Meal, MealRecipe, Ingredient as DomainIngredient, ShoppingList, ShoppingListItem, ShoppingListItemWithRefs, MealInput } from './types' import type { components } from '@/api/types' export function toDate(value: string | Date | null | undefined): Date | null { @@ -6,8 +6,25 @@ export function toDate(value: string | Date | null | undefined): Date | null { return typeof value === 'string' ? new Date(value) : value } -export function decodeRecipe(r: RecipeOut | null | undefined): Recipe | null { - if (!r) return null +// Small helper to decode optional lookup maps without repeating loops everywhere +export function decodeLookup( + raw: Record | null | undefined, + decode: (v: TIn) => TOut +): Record | undefined { + if (!raw) return undefined + const out: Record = {} + for (const key of Object.keys(raw)) { + if (!Object.prototype.hasOwnProperty.call(raw, key)) continue + const maybe = raw[key] + if (maybe === undefined) continue + const decoded = decode(maybe) + out[String(key)] = decoded + } + return out +} + +export function decodeRecipe(r: RecipeOut | null | undefined): Recipe { + if (!r) throw new Error('Invalid recipe payload') return { ...r, dateCreated: toDate(r.dateCreated), @@ -15,15 +32,10 @@ export function decodeRecipe(r: RecipeOut | null | undefined): Recipe | null { } } -export function decodeRecipes(list: RecipeOut[] | null | undefined): Recipe[] { - if (!Array.isArray(list)) return [] - return list.map((r) => decodeRecipe(r)).filter((r): r is Recipe => !!r) -} - -export function decodeMeal(m: MealOut | null | undefined): Meal | null { - if (!m) return null - const recipes = Array.isArray(m.recipes) - ? m.recipes.map(mr => decodeMealRecipe(mr)).filter((r): r is MealRecipe => !!r) +export function decodeMeal(m: MealOut | null | undefined): Meal { + if (!m) throw new Error('Invalid meal payload') + const recipes = Array.isArray(m.recipes) + ? m.recipes.map((mr) => decodeMealRecipe(mr)) : [] return { @@ -39,40 +51,41 @@ export function decodeMeal(m: MealOut | null | undefined): Meal | null { } } -export function decodeMealRecipe(mr: components['schemas']['MealRecipe-Output'] | null | undefined): MealRecipe | null { - if (!mr) return null +function decodeMealRecipe(mr: components['schemas']['MealRecipe-Output'] | null | undefined): MealRecipe { + if (!mr) throw new Error('Invalid meal recipe payload') return { ...mr, recipe: mr.recipe ? decodeRecipe(mr.recipe) : null, } } -export function decodeIngredient(i: components['schemas']['Ingredient'] | null | undefined): DomainIngredient | null { - if (!i) return null +export function decodeIngredient(i: components['schemas']['Ingredient'] | null | undefined): DomainIngredient { + if (!i) throw new Error('Invalid ingredient payload') // API guarantees quantity is a number; pass through return { ...i } } export function decodeIngredients(list: components['schemas']['Ingredient'][] | null | undefined): DomainIngredient[] { if (!Array.isArray(list)) return [] - return list.map((i) => decodeIngredient(i)).filter((x): x is DomainIngredient => !!x) + return list.map((i) => decodeIngredient(i)) } -export function decodeShoppingListItem(i: components['schemas']['ShoppingListItem'] | null | undefined): ShoppingListItem | null { - if (!i) return null +function decodeShoppingListItem(i: components['schemas']['ShoppingListItem'] | null | undefined): ShoppingListItem { + if (!i) throw new Error('Invalid shopping list item payload') return { ...i, createdDate: toDate(i.createdDate), } } -export function decodeShoppingListItems(list: components['schemas']['ShoppingListItem'][] | null | undefined): ShoppingListItem[] { +export function decodeShoppingListItems(list: components['schemas']['ShoppingListItem'][] | null | undefined): ShoppingListItemWithRefs[] { if (!Array.isArray(list)) return [] - return list.map((i) => decodeShoppingListItem(i)).filter((x): x is ShoppingListItem => !!x) + // Build a new array with item clones to allow optional refs to be attached later + return list.map((raw) => ({ ...decodeShoppingListItem(raw) })) } -export function decodeShoppingList(v: components['schemas']['ShoppingList'] | null | undefined): ShoppingList | null { - if (!v) return null +export function decodeShoppingList(v: components['schemas']['ShoppingList'] | null | undefined): ShoppingList { + if (!v) throw new Error('Invalid shopping list payload') const { items: rawItems, ...rest } = v const items = Array.isArray(rawItems) ? decodeShoppingListItems(rawItems) : undefined return { @@ -81,3 +94,23 @@ export function decodeShoppingList(v: components['schemas']['ShoppingList'] | nu ...(items ? { items } : {}), } } + +// Helper to convert domain Meal to MealInput, keeping Date→string conversion in boundary +export function toMealInput(meal: Meal): MealInput { + return { + id: meal.id, + suggestedDate: meal.suggestedDate ? meal.suggestedDate.toISOString() : new Date().toISOString(), + consumedDate: meal.consumedDate ? meal.consumedDate.toISOString() : null, + purchaseDate: meal.purchaseDate ? meal.purchaseDate.toISOString() : null, + chefs: meal.chefs, + cleanup: meal.cleanup, + consumers: meal.consumers, + recipes: meal.recipes.map((r) => ({ + mealId: r.mealId, + recipeId: r.recipeId, + servings: r.servings, + recipe: null, + })), + extraIngredients: meal.extraIngredients, + } +} diff --git a/src/domain/types.ts b/src/domain/types.ts index cbc98de..54c02e3 100644 --- a/src/domain/types.ts +++ b/src/domain/types.ts @@ -4,10 +4,9 @@ import type { components } from '@/api/types' export type Replace = Omit & M export type WithDates = Replace -// Common helpers -export type Maybe = T | null | undefined -export type NonNull = Exclude +// Common helpers (intentionally minimal to avoid unused exports) export type Lookup = Record +// Refs are optional and may be attached later by mappers (e.g., in sdk) export type WithRefs = T & { [K in keyof Refs]?: Refs[K] | undefined } // Domain type aliases @@ -49,7 +48,7 @@ export type ShoppingListItemWithRefs = WithRefs -// Lookup maps used by shopping mappings +// Lookup maps used by shopping mappings (all optional; mappers may omit absent ones) export type ShoppingLookups = { ingredientsLookup?: Lookup mealsLookup?: Lookup diff --git a/src/env.d.ts b/src/env.d.ts index 28ce322..c347a10 100644 --- a/src/env.d.ts +++ b/src/env.d.ts @@ -1,17 +1,10 @@ -/* Ambient env typing for optional Vite-style env access */ -declare interface ImportMeta { - env?: { - VITE_API_BASE_URL?: string - } -} - -export {} -/// - +/* Minimal ambient typing for optional import.meta.env usage in tooling */ interface ImportMetaEnv { readonly VITE_API_BASE_URL?: string } -interface ImportMeta { - readonly env: ImportMetaEnv +declare interface ImportMeta { + readonly env?: ImportMetaEnv } + +export {} diff --git a/src/units.ts b/src/units.ts index c7edf67..144c2d0 100644 --- a/src/units.ts +++ b/src/units.ts @@ -1,7 +1,7 @@ const UNIT_KEYS_ARRAY: readonly ['kg', 'litres', 'items'] = ['kg', 'litres', 'items'] -export type UnitKey = typeof UNIT_KEYS_ARRAY[number] +type UnitKey = typeof UNIT_KEYS_ARRAY[number] -export const equivalentUnits: Record> = { +const equivalentUnits: Record> = { kg: { kgs: 1, kilograms: 1, @@ -124,8 +124,8 @@ export function getConversionFactor(unit: string): { unit: UnitKey | string; fac return null } -export type Quantity = { quantity: number; unit: string } -export type Total = { unit: string; quantity: number } +type Quantity = { quantity: number; unit: string } +type Total = { unit: string; quantity: number } export function calculateTotals(quantityList: Quantity[]): Total[] { const totals: Record = {} diff --git a/tests/test-setup.js b/tests/test-setup.js index a9b8a2d..2d200e5 100644 --- a/tests/test-setup.js +++ b/tests/test-setup.js @@ -2,8 +2,8 @@ import { afterAll, afterEach, beforeAll } from 'vitest' import { setupServer } from 'msw/node' import { http, HttpResponse } from 'msw' -// Ensure global fetch is available in Node tests -// Use Node 18+ global fetch (undici). Do not override so MSW can intercept. +// Ensure global fetch is available in Node tests (Node 18+ provides global fetch) +// Do not override so MSW can intercept. // Some CI envs inject corporate roots causing TLS issues; disable cert checks in tests only process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0' diff --git a/tests/useAlert.test.js b/tests/useAlert.test.js deleted file mode 100644 index 60815d5..0000000 --- a/tests/useAlert.test.js +++ /dev/null @@ -1,7 +0,0 @@ -// Legacy JS test shim: the authoritative tests live in tests/useAlert.test.ts -// Keep a trivial passing test here so Vitest doesn't fail this file collection. -import { it, expect } from 'vitest' - -it('noop shim (see useAlert.test.ts)', () => { - expect(true).toBe(true) -}) diff --git a/tsconfig.json b/tsconfig.json index e111768..0adf0ca 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -10,7 +10,7 @@ "allowJs": false, "checkJs": false, "skipLibCheck": true, - "types": ["node", "vite/client"], + "types": ["node"], "baseUrl": ".", "paths": { "@/*": ["src/*"] diff --git a/vitest.config.js b/vitest.config.js index c43adf8..c9dff06 100644 --- a/vitest.config.js +++ b/vitest.config.js @@ -9,7 +9,7 @@ export default defineConfig({ }, test: { environment: 'node', - include: ['tests/**/*.{test,spec}.js'], + include: ['tests/**/*.{test,spec}.{js,ts}'], globals: true, reporters: 'default', setupFiles: ['tests/test-setup.js'],