chore(sonar): remove the rejected Tinqer spike, clear the lint debt, and report measures in CI (#261) #262
@@ -227,6 +227,24 @@ jobs:
|
|||||||
if: always()
|
if: always()
|
||||||
run: node scripts/summarize-playwright.js frontend/playwright-results.json
|
run: node scripts/summarize-playwright.js frontend/playwright-results.json
|
||||||
|
|
||||||
|
# The measures, printed into the log because that is the only place this
|
||||||
|
# project can read them. SonarQube 9.9 Community has no Bearer auth, so the
|
||||||
|
# official MCP cannot connect, and the host is a CI secret — so security
|
||||||
|
# hotspots, duplication, debt and coverage lived solely on a dashboard, and
|
||||||
|
# "reduce the debt" was an instruction nobody could act on without opening a
|
||||||
|
# browser. The scanner masks the URL and token; the measures are not secret.
|
||||||
|
#
|
||||||
|
# Deliberately not guarded with continue-on-error. The script exits 0 on
|
||||||
|
# every path, so it cannot fail the job anyway, and guarding it would
|
||||||
|
# oblige it to appear in the gate below — which exists to fail the job,
|
||||||
|
# the opposite of what a report should do. See #261.
|
||||||
|
- name: Report SonarQube measures
|
||||||
|
if: always()
|
||||||
|
env:
|
||||||
|
SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}
|
||||||
|
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||||
|
run: node scripts/summarize-sonar.js
|
||||||
|
|
||||||
# Last, so a failing step still produces coverage, a scan and every
|
# Last, so a failing step still produces coverage, a scan and every
|
||||||
# summary first. Without this step continue-on-error above would turn a
|
# summary first. Without this step continue-on-error above would turn a
|
||||||
# failing suite into a passing job, which is the one way this change could
|
# failing suite into a passing job, which is the one way this change could
|
||||||
|
|||||||
Generated
-554
@@ -26,8 +26,6 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.39.5",
|
"@eslint/js": "^9.39.5",
|
||||||
"@tinqerjs/pg-promise-adapter": "^0.0.27",
|
|
||||||
"@tinqerjs/tinqer": "^0.0.27",
|
|
||||||
"@types/bcryptjs": "^2.4.6",
|
"@types/bcryptjs": "^2.4.6",
|
||||||
"@types/cookie-parser": "^1.4.7",
|
"@types/cookie-parser": "^1.4.7",
|
||||||
"@types/express": "^4.17.21",
|
"@types/express": "^4.17.21",
|
||||||
@@ -43,7 +41,6 @@
|
|||||||
"eslint-plugin-sonarjs": "^4.2.0",
|
"eslint-plugin-sonarjs": "^4.2.0",
|
||||||
"globals": "^17.11.0",
|
"globals": "^17.11.0",
|
||||||
"jest": "^29.7.0",
|
"jest": "^29.7.0",
|
||||||
"pg-promise": "^12.7.1",
|
|
||||||
"supertest": "^7.0.0",
|
"supertest": "^7.0.0",
|
||||||
"ts-jest": "^29.2.4",
|
"ts-jest": "^29.2.4",
|
||||||
"tsx": "^4.16.5",
|
"tsx": "^4.16.5",
|
||||||
@@ -635,40 +632,6 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0"
|
"license": "Apache-2.0"
|
||||||
},
|
},
|
||||||
"node_modules/@emnapi/core": {
|
|
||||||
"version": "1.10.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz",
|
|
||||||
"integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@emnapi/wasi-threads": "1.2.1",
|
|
||||||
"tslib": "^2.4.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@emnapi/runtime": {
|
|
||||||
"version": "1.10.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz",
|
|
||||||
"integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"dependencies": {
|
|
||||||
"tslib": "^2.4.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@emnapi/wasi-threads": {
|
|
||||||
"version": "1.2.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz",
|
|
||||||
"integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"dependencies": {
|
|
||||||
"tslib": "^2.4.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild-kit/core-utils": {
|
"node_modules/@esbuild-kit/core-utils": {
|
||||||
"version": "3.3.2",
|
"version": "3.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild-kit/core-utils/-/core-utils-3.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild-kit/core-utils/-/core-utils-3.3.2.tgz",
|
||||||
@@ -2748,28 +2711,6 @@
|
|||||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@napi-rs/wasm-runtime": {
|
|
||||||
"version": "1.2.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.3.tgz",
|
|
||||||
"integrity": "sha512-UMduMbqO5s5zF2NkNacMT/yK5Y5QiKvWr2+50bzIIxFDwVJ2h49b+oyjaCGPhJxd2/gC2x39EHv/gHVuu36x2Q==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@tybys/wasm-util": "^0.10.3"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": "^20.19.0 || ^22.13.0 || >=23.5.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "github",
|
|
||||||
"url": "https://github.com/sponsors/Brooooooklyn"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"@emnapi/core": "^1.7.1 || ^2.0.0-alpha.4",
|
|
||||||
"@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@noble/hashes": {
|
"node_modules/@noble/hashes": {
|
||||||
"version": "1.8.0",
|
"version": "1.8.0",
|
||||||
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz",
|
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz",
|
||||||
@@ -2783,358 +2724,6 @@
|
|||||||
"url": "https://paulmillr.com/funding/"
|
"url": "https://paulmillr.com/funding/"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@oxc-parser/binding-android-arm-eabi": {
|
|
||||||
"version": "0.134.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-android-arm-eabi/-/binding-android-arm-eabi-0.134.0.tgz",
|
|
||||||
"integrity": "sha512-N9Us7l/X9ZC3LA6eWSzPyduvBPXV1eRyDPwM6/UWpxwwXGsatb8131+d2L8UsmyHrixnKLHBd6UeH8wangV7fw==",
|
|
||||||
"cpu": [
|
|
||||||
"arm"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"android"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": "^20.19.0 || >=22.12.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@oxc-parser/binding-android-arm64": {
|
|
||||||
"version": "0.134.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-android-arm64/-/binding-android-arm64-0.134.0.tgz",
|
|
||||||
"integrity": "sha512-Ic2oPZESeCaD4+9cKRqp1GMYsTO9Q3Yi9HdY2x9x75ozbnC20sybFHzeBklmaVD9PBzd8KbkmNN0gy+SVlm7zw==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"android"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": "^20.19.0 || >=22.12.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@oxc-parser/binding-darwin-arm64": {
|
|
||||||
"version": "0.134.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-arm64/-/binding-darwin-arm64-0.134.0.tgz",
|
|
||||||
"integrity": "sha512-1z9+nVJ1Awq4CPyHAthx5zOUrg5T1zc3dWt6juxwDcuejFGbdYzWJITkS1rv4DCdSTphDU3IW71MzyLV3BjRGQ==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"darwin"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": "^20.19.0 || >=22.12.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@oxc-parser/binding-darwin-x64": {
|
|
||||||
"version": "0.134.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-x64/-/binding-darwin-x64-0.134.0.tgz",
|
|
||||||
"integrity": "sha512-MpofofaRZnxmYrY3lE8RTLHmE1KkX2q0elEeJ8sMcLbS8At76BjYSL6axssqhx29prGGDzIZ5lYFD+IXqaTzFA==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"darwin"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": "^20.19.0 || >=22.12.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@oxc-parser/binding-freebsd-x64": {
|
|
||||||
"version": "0.134.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-freebsd-x64/-/binding-freebsd-x64-0.134.0.tgz",
|
|
||||||
"integrity": "sha512-OqnPQY27vqWAbMnHfLcF8CVOUv2cCvdlTiqyK5qz5WCbH3XOLpYVQATv8S5UrR8bbEJCAqJLsI7W6cRFXAxCoA==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"freebsd"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": "^20.19.0 || >=22.12.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@oxc-parser/binding-linux-arm-gnueabihf": {
|
|
||||||
"version": "0.134.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.134.0.tgz",
|
|
||||||
"integrity": "sha512-0eqWl+PWrcwGC3b8DCB58w3QINAuZfpX2ULTGpI01GUMBc6zDKSpttWxvqPIydxuQEkGQTQRAXLLvc+vTDZbQw==",
|
|
||||||
"cpu": [
|
|
||||||
"arm"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": "^20.19.0 || >=22.12.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@oxc-parser/binding-linux-arm-musleabihf": {
|
|
||||||
"version": "0.134.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.134.0.tgz",
|
|
||||||
"integrity": "sha512-YToasuDmyzpyTC1ztrvkaSXz8tP+YUbx041M/4SGxaRGiyMzsKkQ869KPUTGA57A6aVsb1/DiPX8XZQQeSFkiw==",
|
|
||||||
"cpu": [
|
|
||||||
"arm"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": "^20.19.0 || >=22.12.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@oxc-parser/binding-linux-arm64-gnu": {
|
|
||||||
"version": "0.134.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.134.0.tgz",
|
|
||||||
"integrity": "sha512-qMS7NLc2o8G7LLz53wisol+O7/YbMhtaGVhlfsTVLnrraf9kLFgzpiGjtQALLbdxX8uhx0Zmd4l+vY3s1/K4Gg==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": "^20.19.0 || >=22.12.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@oxc-parser/binding-linux-arm64-musl": {
|
|
||||||
"version": "0.134.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.134.0.tgz",
|
|
||||||
"integrity": "sha512-jUEsnxPXhrCYxswQLYvUOxZEE5UWFMkK5kBnrcPMj7TONz1pD0yKgPmOQCAx2LPoysJ/v2Sjg4RBUVoO2VXoZQ==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": "^20.19.0 || >=22.12.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@oxc-parser/binding-linux-ppc64-gnu": {
|
|
||||||
"version": "0.134.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.134.0.tgz",
|
|
||||||
"integrity": "sha512-FU5xMUsXnMuWKVCGo43c6SJsnqHcsioqm32PHdDY39cIRJa/AZbo7RMYW0W5gYcNZqb8EMhELkMDwbJOFbUhtg==",
|
|
||||||
"cpu": [
|
|
||||||
"ppc64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": "^20.19.0 || >=22.12.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@oxc-parser/binding-linux-riscv64-gnu": {
|
|
||||||
"version": "0.134.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.134.0.tgz",
|
|
||||||
"integrity": "sha512-tPc7OAhslHVmNebho1RSEGL//7i7Nm39gbQ+gYreBYwzvDVqNwdQH3S13ccM+R1TpimQ+3bIyFMfnilJIGRtjQ==",
|
|
||||||
"cpu": [
|
|
||||||
"riscv64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": "^20.19.0 || >=22.12.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@oxc-parser/binding-linux-riscv64-musl": {
|
|
||||||
"version": "0.134.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.134.0.tgz",
|
|
||||||
"integrity": "sha512-TtWEC4MUAHodX5a5kGsmK8g5K49V5ewWfwGrXdbw+gXWLXWXuhi+ectNELmOvYIwaqPSTAry1HNS/hfXssaPHQ==",
|
|
||||||
"cpu": [
|
|
||||||
"riscv64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": "^20.19.0 || >=22.12.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@oxc-parser/binding-linux-s390x-gnu": {
|
|
||||||
"version": "0.134.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.134.0.tgz",
|
|
||||||
"integrity": "sha512-mF4uls8TA8SPXSDLpclJ6z9S+vaeUnNp95iUEfqwv9oVJUAE7B2j4crOj8UvByRXpbO5N+aAlJadQEyFlnXU6g==",
|
|
||||||
"cpu": [
|
|
||||||
"s390x"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": "^20.19.0 || >=22.12.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@oxc-parser/binding-linux-x64-gnu": {
|
|
||||||
"version": "0.134.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.134.0.tgz",
|
|
||||||
"integrity": "sha512-hieAQplyJeCvzqdSAxpOOGvVCBVXo/ioIBxioHfsUrQu93Et7Hy52cCG/GHEnjImVyeqEIViyyjuB0nKghxz/w==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": "^20.19.0 || >=22.12.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@oxc-parser/binding-linux-x64-musl": {
|
|
||||||
"version": "0.134.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-x64-musl/-/binding-linux-x64-musl-0.134.0.tgz",
|
|
||||||
"integrity": "sha512-OQnJAK4sFuNSLgsh2s/K+14a3kwbmqf2yh1JiADU9XSfDuRooZYbAxmmBVPiyQ97+jBIIA1x2oPZeYNto3Ioow==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": "^20.19.0 || >=22.12.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@oxc-parser/binding-openharmony-arm64": {
|
|
||||||
"version": "0.134.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-openharmony-arm64/-/binding-openharmony-arm64-0.134.0.tgz",
|
|
||||||
"integrity": "sha512-RYLooe6g31q/PqNFN0NjR80IK/ARGsfLasAXL42LXonL+5Cyy9Or76rjBKQLAjERikJwbRU/sYW9Q5Tnpt1A4Q==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"openharmony"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": "^20.19.0 || >=22.12.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@oxc-parser/binding-wasm32-wasi": {
|
|
||||||
"version": "0.134.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-wasm32-wasi/-/binding-wasm32-wasi-0.134.0.tgz",
|
|
||||||
"integrity": "sha512-h8bmHyvc0PxA811prUjfVpJlQAurOOiRbohY4QNGjCEz+L72G9CmWl28OOz3mevrYlURgUsprNuH8hDJXh1VOw==",
|
|
||||||
"cpu": [
|
|
||||||
"wasm32"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@emnapi/core": "1.10.0",
|
|
||||||
"@emnapi/runtime": "1.10.0",
|
|
||||||
"@napi-rs/wasm-runtime": "^1.1.4"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": "^20.19.0 || >=22.12.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@oxc-parser/binding-win32-arm64-msvc": {
|
|
||||||
"version": "0.134.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.134.0.tgz",
|
|
||||||
"integrity": "sha512-EPTfanpBMLNnSAWCDYpbJp1stmsf5x6hMsAwymf2J8ylRupIvO6FtKmHBMdc/wt43y08iz6ILlzFGIXe32/Kbw==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"win32"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": "^20.19.0 || >=22.12.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@oxc-parser/binding-win32-ia32-msvc": {
|
|
||||||
"version": "0.134.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.134.0.tgz",
|
|
||||||
"integrity": "sha512-yAwetF+fTr9lTMtmqvkkWiiMXvH/yjMxGzUDG2rTL/LV1lL37eZ2enUGIlIo0gwhBIKWgabDEidtil+kiqNpgQ==",
|
|
||||||
"cpu": [
|
|
||||||
"ia32"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"win32"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": "^20.19.0 || >=22.12.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@oxc-parser/binding-win32-x64-msvc": {
|
|
||||||
"version": "0.134.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.134.0.tgz",
|
|
||||||
"integrity": "sha512-4VY39G5tuGlBYiH13XbWqfLcwKygQEv0iyf8vtZ5NyLAGjI8M0MiYyLhj91GkWRznr+5VC0I+5R55HUDV/Rklw==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"win32"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": "^20.19.0 || >=22.12.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@oxc-project/types": {
|
|
||||||
"version": "0.134.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.134.0.tgz",
|
|
||||||
"integrity": "sha512-T0xuRRKrQFmocH8y+jGfpmSkGcheaJExY9lEihmR1Gm2aH+75B8CzgU2rABRQSzzDxLjZ15Sc0bRVLj5lVeNXQ==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/Boshen"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@paralleldrive/cuid2": {
|
"node_modules/@paralleldrive/cuid2": {
|
||||||
"version": "2.3.1",
|
"version": "2.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.3.1.tgz",
|
||||||
@@ -3177,37 +2766,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@stablelib/base64/-/base64-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@stablelib/base64/-/base64-1.0.1.tgz",
|
||||||
"integrity": "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ=="
|
"integrity": "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ=="
|
||||||
},
|
},
|
||||||
"node_modules/@tinqerjs/pg-promise-adapter": {
|
|
||||||
"version": "0.0.27",
|
|
||||||
"resolved": "https://registry.npmjs.org/@tinqerjs/pg-promise-adapter/-/pg-promise-adapter-0.0.27.tgz",
|
|
||||||
"integrity": "sha512-GF2KDUWmavLDCrYRhisvwGR5cah5sSuo3loSunUVTITGvYivlrYl58zOwBKMkC0WfiVyrJ5nVAJ3E/Spt6NH9A==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"peerDependencies": {
|
|
||||||
"@tinqerjs/tinqer": "^0.0.27"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@tinqerjs/tinqer": {
|
|
||||||
"version": "0.0.27",
|
|
||||||
"resolved": "https://registry.npmjs.org/@tinqerjs/tinqer/-/tinqer-0.0.27.tgz",
|
|
||||||
"integrity": "sha512-Rf9zNwstFD1mb8BO6RkRatPbx0rJ0VXHrS9pk8doeVv6sCTgo6wyFR/8hQPYe1LTblJUV3SFQNAfJpvsVbNgrQ==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"oxc-parser": "0.134.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@tybys/wasm-util": {
|
|
||||||
"version": "0.10.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz",
|
|
||||||
"integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"dependencies": {
|
|
||||||
"tslib": "^2.4.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@types/babel__core": {
|
"node_modules/@types/babel__core": {
|
||||||
"version": "7.20.5",
|
"version": "7.20.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
||||||
@@ -4103,16 +3661,6 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/assert-options": {
|
|
||||||
"version": "0.8.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/assert-options/-/assert-options-0.8.3.tgz",
|
|
||||||
"integrity": "sha512-s6v4HnA+vYSGO4eZX+F+I3gvF74wPk+m6Z1Q3w1Dsg4Pnv/R24vhKAasoMVZGvDpOOfTg1Qz4ptZnEbuy95XsQ==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=14.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/asynckit": {
|
"node_modules/asynckit": {
|
||||||
"version": "0.4.0",
|
"version": "0.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||||
@@ -8279,44 +7827,6 @@
|
|||||||
"node": ">= 0.8.0"
|
"node": ">= 0.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/oxc-parser": {
|
|
||||||
"version": "0.134.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/oxc-parser/-/oxc-parser-0.134.0.tgz",
|
|
||||||
"integrity": "sha512-Hs8fRG6A94BzMrMkGOtrUS7JQjmslfF+IvIXslf3QURzK3ud0QmFJRiYZjTe4TzAQnTfvlk4AwZnqIbrUjiE4w==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@oxc-project/types": "^0.134.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": "^20.19.0 || >=22.12.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/Boshen"
|
|
||||||
},
|
|
||||||
"optionalDependencies": {
|
|
||||||
"@oxc-parser/binding-android-arm-eabi": "0.134.0",
|
|
||||||
"@oxc-parser/binding-android-arm64": "0.134.0",
|
|
||||||
"@oxc-parser/binding-darwin-arm64": "0.134.0",
|
|
||||||
"@oxc-parser/binding-darwin-x64": "0.134.0",
|
|
||||||
"@oxc-parser/binding-freebsd-x64": "0.134.0",
|
|
||||||
"@oxc-parser/binding-linux-arm-gnueabihf": "0.134.0",
|
|
||||||
"@oxc-parser/binding-linux-arm-musleabihf": "0.134.0",
|
|
||||||
"@oxc-parser/binding-linux-arm64-gnu": "0.134.0",
|
|
||||||
"@oxc-parser/binding-linux-arm64-musl": "0.134.0",
|
|
||||||
"@oxc-parser/binding-linux-ppc64-gnu": "0.134.0",
|
|
||||||
"@oxc-parser/binding-linux-riscv64-gnu": "0.134.0",
|
|
||||||
"@oxc-parser/binding-linux-riscv64-musl": "0.134.0",
|
|
||||||
"@oxc-parser/binding-linux-s390x-gnu": "0.134.0",
|
|
||||||
"@oxc-parser/binding-linux-x64-gnu": "0.134.0",
|
|
||||||
"@oxc-parser/binding-linux-x64-musl": "0.134.0",
|
|
||||||
"@oxc-parser/binding-openharmony-arm64": "0.134.0",
|
|
||||||
"@oxc-parser/binding-wasm32-wasi": "0.134.0",
|
|
||||||
"@oxc-parser/binding-win32-arm64-msvc": "0.134.0",
|
|
||||||
"@oxc-parser/binding-win32-ia32-msvc": "0.134.0",
|
|
||||||
"@oxc-parser/binding-win32-x64-msvc": "0.134.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/p-limit": {
|
"node_modules/p-limit": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
|
||||||
@@ -8523,17 +8033,6 @@
|
|||||||
"integrity": "sha512-XwWDGcLRGCXAR8F/AM5bG7Q+A3Wm2s6QeEjlOKZLlH3UYcguiqCWKyWXVag5TLTIjR7oOJUY8kcADaZgWPyLeg==",
|
"integrity": "sha512-XwWDGcLRGCXAR8F/AM5bG7Q+A3Wm2s6QeEjlOKZLlH3UYcguiqCWKyWXVag5TLTIjR7oOJUY8kcADaZgWPyLeg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/pg-cursor": {
|
|
||||||
"version": "2.22.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/pg-cursor/-/pg-cursor-2.22.0.tgz",
|
|
||||||
"integrity": "sha512-knzXLKqarTjOvb3qDSW0JiGsazmxwEKXrqHfWRte7XUsOYccQRafn3BLnQobWwInkzFJSyOej8y8cQRh2z3kGw==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"peer": true,
|
|
||||||
"peerDependencies": {
|
|
||||||
"pg": "^8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/pg-int8": {
|
"node_modules/pg-int8": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz",
|
||||||
@@ -8543,16 +8042,6 @@
|
|||||||
"node": ">=4.0.0"
|
"node": ">=4.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/pg-minify": {
|
|
||||||
"version": "1.8.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/pg-minify/-/pg-minify-1.8.0.tgz",
|
|
||||||
"integrity": "sha512-jO/oJOununpx8DzKgvSsWm61P8JjwXlaxSlbbfTBo1nvSWoo/+I6qZYaSN96jm/KDwa5d+JMQwPGgcP6HXDRow==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=16.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/pg-pool": {
|
"node_modules/pg-pool": {
|
||||||
"version": "3.14.0",
|
"version": "3.14.0",
|
||||||
"resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.14.0.tgz",
|
"resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.14.0.tgz",
|
||||||
@@ -8562,45 +8051,12 @@
|
|||||||
"pg": ">=8.0"
|
"pg": ">=8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/pg-promise": {
|
|
||||||
"version": "12.7.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/pg-promise/-/pg-promise-12.7.1.tgz",
|
|
||||||
"integrity": "sha512-tsemfuBRsHMGaFZKMpkmCAA9iLeHXuuTq+0DucJwJyftyXk7crlMFXq7SY0AOVelIrdSL8qwdl5omfj/R8JN6A==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"assert-options": "0.8.3",
|
|
||||||
"pg": "8.23.0",
|
|
||||||
"pg-minify": "1.8.0",
|
|
||||||
"spex": "4.1.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=16.0"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"pg-query-stream": "4.17.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/pg-protocol": {
|
"node_modules/pg-protocol": {
|
||||||
"version": "1.16.0",
|
"version": "1.16.0",
|
||||||
"resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.16.0.tgz",
|
"resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.16.0.tgz",
|
||||||
"integrity": "sha512-sILXutLVjCLjcDuOmvhX5e2Z4cS5qG/6Bu3VkpFwdf/633ElGLpEh9bgmuI5I4sqKqkifQiGyiCcx1HdtrK7tg==",
|
"integrity": "sha512-sILXutLVjCLjcDuOmvhX5e2Z4cS5qG/6Bu3VkpFwdf/633ElGLpEh9bgmuI5I4sqKqkifQiGyiCcx1HdtrK7tg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/pg-query-stream": {
|
|
||||||
"version": "4.17.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/pg-query-stream/-/pg-query-stream-4.17.0.tgz",
|
|
||||||
"integrity": "sha512-69bug2CH/AJi1BN8r08eZ8tAGdbZZVp9kB0FG9OC2VkJPHIyj7d3Nk9V8tmporO60wqeWvhn7qtQ5oNimwlFug==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
|
||||||
"pg-cursor": "^2.22.0"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"pg": "^8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/pg-types": {
|
"node_modules/pg-types": {
|
||||||
"version": "2.2.0",
|
"version": "2.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz",
|
||||||
@@ -9285,16 +8741,6 @@
|
|||||||
"source-map": "^0.6.0"
|
"source-map": "^0.6.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/spex": {
|
|
||||||
"version": "4.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/spex/-/spex-4.1.0.tgz",
|
|
||||||
"integrity": "sha512-ktgNAQ1X9x1A3IMChM6XBDeVjhGPbLgPQ8aEzGOaUIhZTnLeJSBApvi3gXT789hee6h73N3jOeWkXDwoPbYT/A==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/split2": {
|
"node_modules/split2": {
|
||||||
"version": "4.2.0",
|
"version": "4.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
|
||||||
|
|||||||
@@ -45,8 +45,6 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.39.5",
|
"@eslint/js": "^9.39.5",
|
||||||
"@tinqerjs/pg-promise-adapter": "^0.0.27",
|
|
||||||
"@tinqerjs/tinqer": "^0.0.27",
|
|
||||||
"@types/bcryptjs": "^2.4.6",
|
"@types/bcryptjs": "^2.4.6",
|
||||||
"@types/cookie-parser": "^1.4.7",
|
"@types/cookie-parser": "^1.4.7",
|
||||||
"@types/express": "^4.17.21",
|
"@types/express": "^4.17.21",
|
||||||
@@ -62,7 +60,6 @@
|
|||||||
"eslint-plugin-sonarjs": "^4.2.0",
|
"eslint-plugin-sonarjs": "^4.2.0",
|
||||||
"globals": "^17.11.0",
|
"globals": "^17.11.0",
|
||||||
"jest": "^29.7.0",
|
"jest": "^29.7.0",
|
||||||
"pg-promise": "^12.7.1",
|
|
||||||
"supertest": "^7.0.0",
|
"supertest": "^7.0.0",
|
||||||
"ts-jest": "^29.2.4",
|
"ts-jest": "^29.2.4",
|
||||||
"tsx": "^4.16.5",
|
"tsx": "^4.16.5",
|
||||||
|
|||||||
@@ -43,19 +43,11 @@ import { promises as fs } from 'fs';
|
|||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { pool } from './db';
|
import { pool } from './db';
|
||||||
import { needsProcessing, reencodeInPlace } from './imageProcessing';
|
import { needsProcessing, reencodeInPlace } from './imageProcessing';
|
||||||
|
import { typeForExtension } from './uploadTypes';
|
||||||
|
|
||||||
const UPLOADS_DIR = process.env.UPLOADS_DIR || '/app/uploads';
|
const UPLOADS_DIR = process.env.UPLOADS_DIR || '/app/uploads';
|
||||||
const APPLY = process.argv.includes('--apply');
|
const APPLY = process.argv.includes('--apply');
|
||||||
|
|
||||||
// The stored extension is the file's real type — uploadTypes.ts derives it from
|
|
||||||
// the validated content type on the way in, so it can be trusted on the way
|
|
||||||
// back out. Anything else is a file this application would refuse to serve.
|
|
||||||
const TYPE_FOR_EXTENSION: Readonly<Record<string, string>> = {
|
|
||||||
'.jpg': 'image/jpeg',
|
|
||||||
'.png': 'image/png',
|
|
||||||
'.webp': 'image/webp'
|
|
||||||
};
|
|
||||||
|
|
||||||
interface Totals {
|
interface Totals {
|
||||||
seen: number;
|
seen: number;
|
||||||
missing: number;
|
missing: number;
|
||||||
@@ -78,7 +70,12 @@ async function handleRow(imagePath: string, totals: Totals): Promise<void> {
|
|||||||
// basename only: image_path is '/uploads/<name>', and the directory it is
|
// basename only: image_path is '/uploads/<name>', and the directory it is
|
||||||
// served from is a server constant rather than part of the stored value.
|
// served from is a server constant rather than part of the stored value.
|
||||||
const filePath = path.join(UPLOADS_DIR, path.basename(imagePath));
|
const filePath = path.join(UPLOADS_DIR, path.basename(imagePath));
|
||||||
const mimetype = TYPE_FOR_EXTENSION[path.extname(filePath).toLowerCase()];
|
// typeForExtension rather than a copy of its table. The stored extension is
|
||||||
|
// the file's real type — uploadTypes.ts derives it from the validated content
|
||||||
|
// type on the way in — and this rewrites stored images, so a private copy
|
||||||
|
// drifting from the real one would silently skip files it should re-encode.
|
||||||
|
// It lowercases its own input, so the call site does not.
|
||||||
|
const mimetype = typeForExtension(path.extname(filePath));
|
||||||
|
|
||||||
if (!mimetype) {
|
if (!mimetype) {
|
||||||
console.warn(`[backfill] unrecognised extension, skipping: ${imagePath}`);
|
console.warn(`[backfill] unrecognised extension, skipping: ${imagePath}`);
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
// match with a count equality, and array parameters. If this cannot be said
|
// match with a count equality, and array parameters. If this cannot be said
|
||||||
// cleanly, nothing else in the conversion matters.
|
// cleanly, nothing else in the conversion matters.
|
||||||
|
|
||||||
import { SQL, and, eq, gte, lte, sql, inArray, exists } from 'drizzle-orm';
|
import { SQL, and, gte, lte, sql, inArray, exists } from 'drizzle-orm';
|
||||||
import { items, itemTags, favorites, categories } from './schema';
|
import { items, itemTags, favorites, categories } from './schema';
|
||||||
|
|
||||||
export interface SpikeFilters {
|
export interface SpikeFilters {
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ export const tags = pgTable("tags", {
|
|||||||
name: text().notNull(),
|
name: text().notNull(),
|
||||||
color: text().notNull(),
|
color: text().notNull(),
|
||||||
createdAt: timestamp("created_at", { withTimezone: true, mode: 'string' }).defaultNow().notNull(),
|
createdAt: timestamp("created_at", { withTimezone: true, mode: 'string' }).defaultNow().notNull(),
|
||||||
}, (table) => [
|
}, () => [
|
||||||
uniqueIndex("tags_name_uniq").using("btree", sql`lower(name)`),
|
uniqueIndex("tags_name_uniq").using("btree", sql`lower(name)`),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
@@ -1,71 +0,0 @@
|
|||||||
// Spike (#216): what Tinqer can and cannot express, recorded as runnable cases.
|
|
||||||
//
|
|
||||||
// Every case is wrapped in a function rather than evaluated at module level.
|
|
||||||
// defineSelect() parses the lambda eagerly and THROWS on a shape it cannot
|
|
||||||
// handle, so a top-level call turns an unsupported query into an import-time
|
|
||||||
// crash. That is itself a finding worth keeping: the failure is a runtime throw
|
|
||||||
// at load, not a type error, so an unsupported query compiles cleanly and takes
|
|
||||||
// the process down when the module is first required.
|
|
||||||
|
|
||||||
import { createSchema, defineSelect } from '@tinqerjs/tinqer';
|
|
||||||
import { toSql } from '@tinqerjs/pg-promise-adapter';
|
|
||||||
|
|
||||||
interface Db {
|
|
||||||
items: { id: number; name: string; price_cents: number; status: string; category_id: number | null };
|
|
||||||
categories: { id: number; parent_id: number | null };
|
|
||||||
}
|
|
||||||
|
|
||||||
const schema = createSchema<Db>();
|
|
||||||
|
|
||||||
export type Outcome = { label: string; ok: boolean; sql?: string; error?: string };
|
|
||||||
|
|
||||||
function attempt(label: string, build: () => unknown, params: Record<string, unknown>): Outcome {
|
|
||||||
try {
|
|
||||||
const r = toSql(build() as never, params as never);
|
|
||||||
return { label, ok: true, sql: r.sql };
|
|
||||||
} catch (e) {
|
|
||||||
return { label, ok: false, error: (e as Error).message.split('\n')[0] };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function runCases(): Outcome[] {
|
|
||||||
return [
|
|
||||||
// Supported.
|
|
||||||
attempt(
|
|
||||||
'compound condition',
|
|
||||||
() =>
|
|
||||||
defineSelect(schema, (q, p: { min: number; max: number }) =>
|
|
||||||
q.from('items').where((i) => i.price_cents >= p.min && i.price_cents <= p.max).select((i) => ({ id: i.id }))
|
|
||||||
),
|
|
||||||
{ min: 1, max: 2 }
|
|
||||||
),
|
|
||||||
attempt(
|
|
||||||
'array membership',
|
|
||||||
() =>
|
|
||||||
defineSelect(schema, (q, p: { statuses: string[] }) =>
|
|
||||||
q.from('items').where((i) => p.statuses.includes(i.status)).select((i) => ({ id: i.id }))
|
|
||||||
),
|
|
||||||
{ statuses: ['available', 'sold'] }
|
|
||||||
),
|
|
||||||
|
|
||||||
// NOT supported — and these are the two shapes buildItemFilterSql needs.
|
|
||||||
attempt(
|
|
||||||
'ternary (clause optional at run time)',
|
|
||||||
() =>
|
|
||||||
defineSelect(schema, (q, p: { apply: boolean; min: number }) =>
|
|
||||||
q.from('items').where((i) => (p.apply ? i.price_cents >= p.min : true)).select((i) => ({ id: i.id }))
|
|
||||||
),
|
|
||||||
{ apply: true, min: 1 }
|
|
||||||
),
|
|
||||||
attempt(
|
|
||||||
'block body with if (clause optional at run time)',
|
|
||||||
() =>
|
|
||||||
defineSelect(schema, (q, p: { apply: boolean; min: number }) => {
|
|
||||||
let x = q.from('items');
|
|
||||||
if (p.apply) x = x.where((i) => i.price_cents >= p.min);
|
|
||||||
return x.select((i) => ({ id: i.id }));
|
|
||||||
}),
|
|
||||||
{ apply: true, min: 1 }
|
|
||||||
)
|
|
||||||
];
|
|
||||||
}
|
|
||||||
@@ -2,6 +2,11 @@ import { Request, Response, NextFunction } from 'express';
|
|||||||
import { pool } from '../db';
|
import { pool } from '../db';
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
// A namespace is the only way to spell an Express type augmentation — the
|
||||||
|
// interface has to merge into the one Express declares, and Express declares
|
||||||
|
// it inside a namespace. There is no ES module form of this, so the rule is
|
||||||
|
// disabled here rather than worked around.
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||||
namespace Express {
|
namespace Express {
|
||||||
interface Request {
|
interface Request {
|
||||||
customerId?: number;
|
customerId?: number;
|
||||||
|
|||||||
@@ -118,6 +118,10 @@ router.post('/:id/set-default', requireCustomer, asyncRoute(async (req: Request,
|
|||||||
res.json(rows[0]);
|
res.json(rows[0]);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
await client.query('ROLLBACK');
|
await client.query('ROLLBACK');
|
||||||
|
// Logged like the two catch blocks above it in this file, which this one
|
||||||
|
// was simply missing. Without it a failed default-address change rolls
|
||||||
|
// back and returns 500 leaving nothing behind to say why.
|
||||||
|
console.error(err);
|
||||||
res.status(500).json({ error: 'internal error' });
|
res.status(500).json({ error: 'internal error' });
|
||||||
} finally {
|
} finally {
|
||||||
client.release();
|
client.release();
|
||||||
|
|||||||
@@ -56,8 +56,15 @@ function isPublicStatus(value: string): value is ItemStatus {
|
|||||||
return value === 'available' || value === 'reserved' || value === 'sold';
|
return value === 'available' || value === 'reserved' || value === 'sold';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sorted with an explicit comparator rather than a bare .sort(). The default
|
||||||
|
// sorts by UTF-16 code unit, which is a perfectly good total order for the
|
||||||
|
// ASCII status values passed here — but the rule exists because that stops
|
||||||
|
// being true the moment a non-ASCII value appears, and a set comparison that
|
||||||
|
// silently depends on its inputs staying ASCII is not worth keeping.
|
||||||
const sameSet = (a: readonly string[], b: readonly string[]) =>
|
const sameSet = (a: readonly string[], b: readonly string[]) =>
|
||||||
a.length === b.length && [...a].sort().join() === [...b].sort().join();
|
a.length === b.length &&
|
||||||
|
[...a].sort((x, y) => x.localeCompare(y)).join() ===
|
||||||
|
[...b].sort((x, y) => x.localeCompare(y)).join();
|
||||||
|
|
||||||
// Which preset a status list corresponds to, for showing the control's current
|
// Which preset a status list corresponds to, for showing the control's current
|
||||||
// position. Null means no preference, which each screen renders as its default.
|
// position. Null means no preference, which each screen renders as its default.
|
||||||
|
|||||||
@@ -0,0 +1,196 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
// Prints the project's SonarQube measures into the job log.
|
||||||
|
//
|
||||||
|
// This exists because the numbers are otherwise unreachable. The server is
|
||||||
|
// SonarQube 9.9 Community: it has no Bearer auth, so the official MCP cannot
|
||||||
|
// connect, and the host URL is a CI secret, so there is no way to query it from
|
||||||
|
// a developer's machine either. The measures lived only on a dashboard, which
|
||||||
|
// made "reduce the debt" an instruction nobody could act on without a browser
|
||||||
|
// open beside them. See #261.
|
||||||
|
//
|
||||||
|
// The scanner masks SONAR_HOST_URL and SONAR_TOKEN in the log. Measures are not
|
||||||
|
// secret, so they print fine — only the URL they came from is redacted.
|
||||||
|
//
|
||||||
|
// Authentication is HTTP Basic with the token as the username and an empty
|
||||||
|
// password. That is the documented scheme for 9.9; the `Authorization: Bearer`
|
||||||
|
// form was added later and returns 401 here.
|
||||||
|
//
|
||||||
|
// Runs with `if: always()` and deliberately WITHOUT `continue-on-error`. A
|
||||||
|
// cleanup report that can break a build is worse than no report, so instead of
|
||||||
|
// being guarded this script simply never fails: every path exits 0 and problems
|
||||||
|
// are described rather than thrown. Guarding it would also oblige it to be named
|
||||||
|
// in the workflow's final gate — workflowGate.test.ts asserts that pairing — and
|
||||||
|
// that gate exists to fail the job, which is the opposite of what is wanted for
|
||||||
|
// a report. Same shape as the summarize-jest.js steps beside it.
|
||||||
|
|
||||||
|
const https = require('https');
|
||||||
|
const http = require('http');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const HOST = process.env.SONAR_HOST_URL;
|
||||||
|
const TOKEN = process.env.SONAR_TOKEN;
|
||||||
|
const PROJECT = process.argv[2] || 'redefined-designs';
|
||||||
|
|
||||||
|
// Ordered so the four things the cleanup issue asks about come first, rather
|
||||||
|
// than alphabetically. Each row is [metric key, label, formatter].
|
||||||
|
const METRICS = [
|
||||||
|
['security_hotspots', 'Security hotspots', (v) => v],
|
||||||
|
['vulnerabilities', 'Vulnerabilities', (v) => v],
|
||||||
|
['bugs', 'Bugs', (v) => v],
|
||||||
|
['duplicated_lines_density', 'Duplicated lines', (v) => `${v}%`],
|
||||||
|
['duplicated_blocks', 'Duplicated blocks', (v) => v],
|
||||||
|
['sqale_index', 'Technical debt', formatDebt],
|
||||||
|
['code_smells', 'Code smells', (v) => v],
|
||||||
|
['coverage', 'Coverage', (v) => `${v}%`],
|
||||||
|
['line_coverage', 'Line coverage', (v) => `${v}%`],
|
||||||
|
['branch_coverage', 'Branch coverage', (v) => `${v}%`],
|
||||||
|
['uncovered_lines', 'Uncovered lines', (v) => v],
|
||||||
|
['ncloc', 'Lines of code', (v) => v],
|
||||||
|
['new_coverage', 'Coverage on new code', (v) => `${v}%`],
|
||||||
|
['new_duplicated_lines_density', 'Duplication on new code', (v) => `${v}%`],
|
||||||
|
['new_code_smells', 'Code smells on new code', (v) => v]
|
||||||
|
];
|
||||||
|
|
||||||
|
/** sqale_index is minutes. Days here are Sonar's 8-hour working days. */
|
||||||
|
function formatDebt(minutes) {
|
||||||
|
const total = Number(minutes);
|
||||||
|
if (!Number.isFinite(total)) return String(minutes);
|
||||||
|
const days = Math.floor(total / (8 * 60));
|
||||||
|
const hours = Math.floor((total % (8 * 60)) / 60);
|
||||||
|
const mins = total % 60;
|
||||||
|
return `${total} min (${days}d ${hours}h ${mins}m)`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function get(url) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const client = url.startsWith('https:') ? https : http;
|
||||||
|
const request = client.get(
|
||||||
|
url,
|
||||||
|
// The empty password is deliberate: 9.9 expects `token:` rather than a
|
||||||
|
// password, and omitting the colon sends the token as a username with no
|
||||||
|
// password field at all, which it rejects.
|
||||||
|
{ auth: `${TOKEN}:`, timeout: 30000 },
|
||||||
|
(res) => {
|
||||||
|
let body = '';
|
||||||
|
res.on('data', (chunk) => (body += chunk));
|
||||||
|
res.on('end', () => resolve({ status: res.statusCode, body }));
|
||||||
|
}
|
||||||
|
);
|
||||||
|
request.on('timeout', () => {
|
||||||
|
request.destroy();
|
||||||
|
resolve({ status: 0, body: 'timed out after 30s' });
|
||||||
|
});
|
||||||
|
request.on('error', (err) => resolve({ status: 0, body: err.message }));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function parse(body) {
|
||||||
|
try {
|
||||||
|
return JSON.parse(body);
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Waits for the server to finish processing this run's analysis.
|
||||||
|
*
|
||||||
|
* The workflow does not set `sonar.qualitygate.wait`, so the scan step returns
|
||||||
|
* as soon as the report is uploaded and the server computes measures in the
|
||||||
|
* background. Querying immediately therefore returns the *previous* analysis,
|
||||||
|
* which would be indistinguishable from the current one and quietly wrong — the
|
||||||
|
* worst kind of number to publish into a cleanup report.
|
||||||
|
*
|
||||||
|
* The scanner writes the task id to .scannerwork/report-task.txt. Polling it
|
||||||
|
* costs a few seconds and makes the difference between "these are this commit's
|
||||||
|
* numbers" and "these are probably this commit's numbers".
|
||||||
|
*/
|
||||||
|
async function waitForAnalysis(base) {
|
||||||
|
const reportPath = path.join(process.cwd(), '.scannerwork', 'report-task.txt');
|
||||||
|
if (!fs.existsSync(reportPath)) {
|
||||||
|
return 'no report-task.txt — reporting the last completed analysis, which may predate this commit';
|
||||||
|
}
|
||||||
|
|
||||||
|
const taskId = /^ceTaskId=(.+)$/m.exec(fs.readFileSync(reportPath, 'utf8'))?.[1]?.trim();
|
||||||
|
if (!taskId) {
|
||||||
|
return 'no ceTaskId in report-task.txt — reporting the last completed analysis';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Twenty attempts at three seconds. A minute is far longer than this project
|
||||||
|
// takes to process and short enough not to stretch the job noticeably.
|
||||||
|
for (let attempt = 0; attempt < 20; attempt++) {
|
||||||
|
const res = await get(`${base}/api/ce/task?id=${encodeURIComponent(taskId)}`);
|
||||||
|
const status = parse(res.body)?.task?.status;
|
||||||
|
if (status === 'SUCCESS') return null;
|
||||||
|
if (status === 'FAILED' || status === 'CANCELED') {
|
||||||
|
return `analysis ${status} on the server — reporting the last completed analysis instead`;
|
||||||
|
}
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 3000));
|
||||||
|
}
|
||||||
|
return 'analysis still processing after 60s — reporting the last completed analysis';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
if (!HOST || !TOKEN) {
|
||||||
|
// Not an error. A fork or a run without the secrets configured reaches this
|
||||||
|
// legitimately, and saying so is more useful than a stack trace.
|
||||||
|
console.log('SonarQube measures: SONAR_HOST_URL or SONAR_TOKEN not set, skipping.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const base = HOST.replace(/\/+$/, '');
|
||||||
|
|
||||||
|
const staleness = await waitForAnalysis(base);
|
||||||
|
|
||||||
|
const keys = METRICS.map(([key]) => key).join(',');
|
||||||
|
const measuresUrl = `${base}/api/measures/component?component=${encodeURIComponent(PROJECT)}&metricKeys=${keys}`;
|
||||||
|
|
||||||
|
const res = await get(measuresUrl);
|
||||||
|
if (res.status !== 200) {
|
||||||
|
console.log(`SonarQube measures: request failed (status ${res.status}). ${res.body.slice(0, 300)}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const json = parse(res.body);
|
||||||
|
const measures = json?.component?.measures;
|
||||||
|
if (!Array.isArray(measures)) {
|
||||||
|
console.log('SonarQube measures: unexpected response shape.');
|
||||||
|
console.log(res.body.slice(0, 300));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keyed by metric so the display order above is what prints, rather than
|
||||||
|
// whatever order the API happened to return.
|
||||||
|
const byKey = new Map(measures.map((m) => [m.metric, m]));
|
||||||
|
|
||||||
|
const lines = ['', '=== SonarQube measures ===', ''];
|
||||||
|
// Said plainly rather than omitted. A reader acting on these numbers needs to
|
||||||
|
// know whether they describe this commit.
|
||||||
|
if (staleness) lines.push(`NOTE: ${staleness}`, '');
|
||||||
|
for (const [key, label, format] of METRICS) {
|
||||||
|
const measure = byKey.get(key);
|
||||||
|
// A metric can be legitimately absent — new-code metrics do not exist until
|
||||||
|
// a second analysis, and branch_coverage is missing when nothing branches.
|
||||||
|
if (!measure) {
|
||||||
|
lines.push(`${label.padEnd(26)} —`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const value = measure.period?.value ?? measure.value;
|
||||||
|
lines.push(`${label.padEnd(26)} ${format(value)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const gate = await get(
|
||||||
|
`${base}/api/qualitygates/project_status?projectKey=${encodeURIComponent(PROJECT)}`
|
||||||
|
);
|
||||||
|
const gateStatus = parse(gate.body)?.projectStatus?.status;
|
||||||
|
lines.push('', `Quality gate ${gateStatus ?? 'unknown'}`, '');
|
||||||
|
|
||||||
|
console.log(lines.join('\n'));
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((err) => {
|
||||||
|
// Reported rather than thrown, for the reason in the header: this step must
|
||||||
|
// not be able to fail the job.
|
||||||
|
console.log(`SonarQube measures: ${err instanceof Error ? err.message : String(err)}`);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user