diff --git a/nodejs/sqlcommenter-nodejs/packages/sqlcommenter-drizzle/README.md b/nodejs/sqlcommenter-nodejs/packages/sqlcommenter-drizzle/README.md index 586bf531..adc33703 100644 --- a/nodejs/sqlcommenter-nodejs/packages/sqlcommenter-drizzle/README.md +++ b/nodejs/sqlcommenter-nodejs/packages/sqlcommenter-drizzle/README.md @@ -77,16 +77,55 @@ app.use((c, next) => { #### Fastify +The recommended way is the first-party plugin, which wires everything correctly: + +```ts +import { sqlcommenterFastify } from "@query-doctor/sqlcommenter-drizzle/fastify"; + +// Register it BEFORE any plugin whose hooks issue queries (e.g. auth), so the context +// is already open when those hooks run. +await app.register(sqlcommenterFastify); +await app.register(authPlugin); +``` + +It hooks `onRequest`, so it tags queries from the **entire request lifecycle** — including queries +issued in other plugins' `onRequest`/`preHandler` hooks — not just the route handler. Pass +`context` to add extra fields: + +```ts +await app.register(sqlcommenterFastify, { + context: (request) => ({ controller: "items" }), +}); +``` + +##### Doing it manually + +If you'd rather wire it yourself with `withRequestContext`, two things are easy to get wrong: + +- **Register the hook globally with [`fastify-plugin`](https://github.com/fastify/fastify-plugin).** + A plain `app.register(plugin)` encapsulates the `onRequest` hook, so it silently does **not** + apply to routes registered in the parent scope — you get no `route`/`method` tags and no error. +- **Hook `onRequest` (not the route handler), registered before any plugin whose hooks issue + queries** (e.g. an auth plugin that resolves a session in its own `onRequest`/`preHandler`). + Wrapping only the handler misses those earlier queries. + ```ts +import fp from "fastify-plugin"; import { withRequestContext } from "@query-doctor/sqlcommenter-drizzle/http"; -app.addHook("onRequest", (request, _, done) => { - withRequestContext( - { - route: request.routerPath, - method: request.method, - }, - done - ); +const sqlcommenter = fp((app, _opts, done) => { + app.addHook("onRequest", (request, _reply, next) => { + withRequestContext( + { + // `routerPath` was removed in Fastify v5; `routeOptions.url` is the matched route pattern. + route: request.routeOptions?.url ?? request.url, + method: request.method, + }, + next + ); + }); + done(); }); + +await app.register(sqlcommenter); // before auth, etc. ``` diff --git a/nodejs/sqlcommenter-nodejs/packages/sqlcommenter-drizzle/package-lock.json b/nodejs/sqlcommenter-nodejs/packages/sqlcommenter-drizzle/package-lock.json index bc9e2adb..05fdaa36 100644 --- a/nodejs/sqlcommenter-nodejs/packages/sqlcommenter-drizzle/package-lock.json +++ b/nodejs/sqlcommenter-nodejs/packages/sqlcommenter-drizzle/package-lock.json @@ -1,12 +1,12 @@ { "name": "@query-doctor/sqlcommenter-drizzle", - "version": "0.3.0", + "version": "0.4.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@query-doctor/sqlcommenter-drizzle", - "version": "0.3.0", + "version": "0.4.0", "license": "Apache-2.0", "dependencies": { "@opentelemetry/api": "~1.9.0" @@ -14,6 +14,7 @@ "devDependencies": { "@electric-sql/pglite": "^0.3.10", "@types/node": "^20.19.34", + "fastify": "^5.2.0", "hono": "^4.9.8", "postgres": "^3.4.7", "rewiremock": "^3.14.3", @@ -25,7 +26,13 @@ }, "peerDependencies": { "@opentelemetry/core": ">=2.8.0", - "drizzle-orm": ">=0.35.0" + "drizzle-orm": ">=0.35.0", + "fastify": ">=4.0.0" + }, + "peerDependenciesMeta": { + "fastify": { + "optional": true + } } }, "node_modules/@electric-sql/pglite": { @@ -477,6 +484,123 @@ "node": ">=18" } }, + "node_modules/@fastify/ajv-compiler": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@fastify/ajv-compiler/-/ajv-compiler-4.0.5.tgz", + "integrity": "sha512-KoWKW+MhvfTRWL4qrhUwAAZoaChluo0m0vbiJlGMt2GXvL4LVPQEjt8kSpHI3IBq5Rez8fg+XeH3cneztq+C7A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "ajv": "^8.12.0", + "ajv-formats": "^3.0.1", + "fast-uri": "^3.0.0" + } + }, + "node_modules/@fastify/error": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@fastify/error/-/error-4.2.0.tgz", + "integrity": "sha512-RSo3sVDXfHskiBZKBPRgnQTtIqpi/7zhJOEmAxCiBcM7d0uwdGdxLlsCaLzGs8v8NnxIRlfG0N51p5yFaOentQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/@fastify/fast-json-stringify-compiler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@fastify/fast-json-stringify-compiler/-/fast-json-stringify-compiler-5.0.3.tgz", + "integrity": "sha512-uik7yYHkLr6fxd8hJSZ8c+xF4WafPK+XzneQDPU+D10r5X19GW8lJcom2YijX2+qtFF1ENJlHXKFM9ouXNJYgQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "fast-json-stringify": "^6.0.0" + } + }, + "node_modules/@fastify/forwarded": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@fastify/forwarded/-/forwarded-3.0.1.tgz", + "integrity": "sha512-JqDochHFqXs3C3Ml3gOY58zM7OqO9ENqPo0UqAjAjH8L01fRZqwX9iLeX34//kiJubF7r2ZQHtBRU36vONbLlw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/@fastify/merge-json-schemas": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@fastify/merge-json-schemas/-/merge-json-schemas-0.2.1.tgz", + "integrity": "sha512-OA3KGBCy6KtIvLf8DINC5880o5iBlDX4SxzLQS8HorJAbqluzLRn80UXU0bxZn7UOFhFgpRJDasfwn9nG4FG4A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/@fastify/proxy-addr": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@fastify/proxy-addr/-/proxy-addr-5.1.0.tgz", + "integrity": "sha512-INS+6gh91cLUjB+PVHfu1UqcB76Sqtpyp7bnL+FYojhjygvOPA9ctiD/JDKsyD9Xgu4hUhCSJBPig/w7duNajw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/forwarded": "^3.0.0", + "ipaddr.js": "^2.1.0" + } + }, "node_modules/@opentelemetry/api": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", @@ -512,6 +636,13 @@ "node": ">=14" } }, + "node_modules/@pinojs/redact": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz", + "integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { "version": "20.19.34", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.34.tgz", @@ -522,6 +653,48 @@ "undici-types": "~6.21.0" } }, + "node_modules/abstract-logging": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz", + "integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==", + "dev": true, + "license": "MIT" + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, "node_modules/asn1.js": { "version": "4.10.1", "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz", @@ -569,6 +742,16 @@ "inherits": "2.0.3" } }, + "node_modules/atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/available-typed-arrays": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", @@ -585,6 +768,27 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/avvio": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/avvio/-/avvio-9.2.0.tgz", + "integrity": "sha512-2t/sy01ArdHHE0vRH5Hsay+RtCZt3dLPji7W7/MMOCEgze5b7SNDC4j5H6FnVgPkI1MTNFGzHdHrVXDDl7QSSQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/error": "^4.0.0", + "fastq": "^1.17.1" + } + }, "node_modules/babel-runtime": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", @@ -828,6 +1032,20 @@ "dev": true, "license": "MIT" }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/core-js": { "version": "2.6.12", "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.12.tgz", @@ -954,6 +1172,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/des.js": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.1.0.tgz", @@ -1255,6 +1483,131 @@ "safe-buffer": "^5.1.1" } }, + "node_modules/fast-decode-uri-component": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz", + "integrity": "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stringify": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/fast-json-stringify/-/fast-json-stringify-6.4.0.tgz", + "integrity": "sha512-ibRCQ0GZKJIQ+P3Et1h0LhPgp3PMTYk0MH8O+kW3lNYsvmaQww5Nn3f1jf73Q0jR1Yz3a1CDP4/NZD3vOajWJQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/merge-json-schemas": "^0.2.0", + "ajv": "^8.12.0", + "ajv-formats": "^3.0.1", + "fast-uri": "^3.0.0", + "json-schema-ref-resolver": "^3.0.0", + "rfdc": "^1.2.0" + } + }, + "node_modules/fast-querystring": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/fast-querystring/-/fast-querystring-1.1.2.tgz", + "integrity": "sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-decode-uri-component": "^1.0.1" + } + }, + "node_modules/fast-uri": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.3.tgz", + "integrity": "sha512-i70LwGWUduXqzicKXWshooq+sWL1K3WUU5rKZNG/0i3a1OSoX3HqhH5WbWwTmqWfor4urUakGPiRQcleRZTwOg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fastify": { + "version": "5.9.0", + "resolved": "https://registry.npmjs.org/fastify/-/fastify-5.9.0.tgz", + "integrity": "sha512-VMS5lE0zj+MZlJpQa3Qv5iGjfun0H2N7VRgoBwpcTNQ2bdIQpv7fDpb+HGteGbicBsGkzGS+X+hdx9mmrfWuHQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/ajv-compiler": "^4.0.5", + "@fastify/error": "^4.0.0", + "@fastify/fast-json-stringify-compiler": "^5.0.0", + "@fastify/proxy-addr": "^5.0.0", + "abstract-logging": "^2.0.1", + "avvio": "^9.0.0", + "fast-json-stringify": "^6.0.0", + "find-my-way": "^9.6.0", + "light-my-request": "^6.0.0", + "pino": "^9.14.0 || ^10.1.0", + "process-warning": "^5.0.0", + "rfdc": "^1.3.1", + "secure-json-parse": "^4.0.0", + "semver": "^7.6.0", + "toad-cache": "^3.7.0" + } + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/find-my-way": { + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-9.6.0.tgz", + "integrity": "sha512-Zf4Xve4RymLl7NgaavNebZ01joJ8MfVerOG43wy7SHLO+r+K0C6d/SE0BiR7AV5V1VOCFlOP7ecdo+I4qmiHrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-querystring": "^1.0.0", + "safe-regex2": "^5.0.0" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/for-each": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", @@ -1498,6 +1851,16 @@ "dev": true, "license": "ISC" }, + "node_modules/ipaddr.js": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.4.0.tgz", + "integrity": "sha512-9VGk3HGanVE6JoZXHiCpnGy5X0jYDnN4EA4lntFPj+1vIWlFhIylq2CrrCOJH9EAhc5CYhq18F2Av2tgoAPsYQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, "node_modules/is-callable": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", @@ -1534,6 +1897,72 @@ "dev": true, "license": "MIT" }, + "node_modules/json-schema-ref-resolver": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/json-schema-ref-resolver/-/json-schema-ref-resolver-3.0.0.tgz", + "integrity": "sha512-hOrZIVL5jyYFjzk7+y7n5JDzGlU8rfWDuYyHwGa2WA8/pcmMHezp2xsVwxrebD/Q9t8Nc5DboieySDpCp4WG4A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/light-my-request": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/light-my-request/-/light-my-request-6.6.0.tgz", + "integrity": "sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause", + "dependencies": { + "cookie": "^1.0.1", + "process-warning": "^4.0.0", + "set-cookie-parser": "^2.6.0" + } + }, + "node_modules/light-my-request/node_modules/process-warning": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-4.0.1.tgz", + "integrity": "sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -1667,6 +2096,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/on-exit-leak-free": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/os-browserify": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz", @@ -1730,6 +2169,46 @@ "node": ">= 0.10" } }, + "node_modules/pino": { + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/pino/-/pino-10.3.1.tgz", + "integrity": "sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pinojs/redact": "^0.4.0", + "atomic-sleep": "^1.0.0", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^3.0.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^5.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^4.0.1", + "thread-stream": "^4.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/pino-abstract-transport": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-3.0.0.tgz", + "integrity": "sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "split2": "^4.0.0" + } + }, + "node_modules/pino-std-serializers": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.1.0.tgz", + "integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==", + "dev": true, + "license": "MIT" + }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", @@ -1771,6 +2250,23 @@ "dev": true, "license": "MIT" }, + "node_modules/process-warning": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz", + "integrity": "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, "node_modules/public-encrypt": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz", @@ -1825,6 +2321,13 @@ "node": ">=0.4.x" } }, + "node_modules/quick-format-unescaped": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", + "dev": true, + "license": "MIT" + }, "node_modules/randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -1879,6 +2382,16 @@ "safe-buffer": "~5.1.0" } }, + "node_modules/real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12.13.0" + } + }, "node_modules/regenerator-runtime": { "version": "0.11.1", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", @@ -1886,6 +2399,16 @@ "dev": true, "license": "MIT" }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/resolve-pkg-maps": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", @@ -1896,6 +2419,27 @@ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } }, + "node_modules/ret": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.5.0.tgz", + "integrity": "sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, "node_modules/rewiremock": { "version": "3.14.6", "resolved": "https://registry.npmjs.org/rewiremock/-/rewiremock-3.14.6.tgz", @@ -1911,6 +2455,13 @@ "wipe-webpack-cache": "^2.1.0" } }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "dev": true, + "license": "MIT" + }, "node_modules/ripemd160": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.3.tgz", @@ -1962,6 +2513,76 @@ ], "license": "MIT" }, + "node_modules/safe-regex2": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/safe-regex2/-/safe-regex2-5.1.1.tgz", + "integrity": "sha512-mOSBvHGDZMuIEZMdOz/aCEYDCv0E7nfcNsIhUF+/P+xC7Hyf3FkvymqgPbg9D1EdSGu+uKbJgy09K/RKKc7kJA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "ret": "~0.5.0" + }, + "bin": { + "safe-regex2": "bin/safe-regex2.js" + } + }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/secure-json-parse": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-4.1.0.tgz", + "integrity": "sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "dev": true, + "license": "MIT" + }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -2084,6 +2705,26 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/sonic-boom": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz", + "integrity": "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, "node_modules/stream-browserify": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.2.tgz", @@ -2119,6 +2760,26 @@ "safe-buffer": "~5.2.0" } }, + "node_modules/thread-stream": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-4.2.0.tgz", + "integrity": "sha512-e2zZ96wSChazBsbENf/Pcm/4swHt2cEKQ92rhUjkL9GCKiTDJIaTBenjE/m9DXi0QBmTMDkFDdOomUy20A1tDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "real-require": "^1.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/thread-stream/node_modules/real-require": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-1.0.0.tgz", + "integrity": "sha512-P4nbQYQfePJxRSmY+v/KINxVucm4NF3p3s7pJveMTtom52FR4YGltUQLB8idDXwDDWW+eYrWDFbuzUnjoWHF7g==", + "dev": true, + "license": "MIT" + }, "node_modules/timers-browserify": { "version": "2.0.12", "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.12.tgz", @@ -2161,6 +2822,16 @@ "dev": true, "license": "MIT" }, + "node_modules/toad-cache": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/toad-cache/-/toad-cache-3.7.1.tgz", + "integrity": "sha512-5DXWzE4Vz7xNHsv+xQ+MGfJYyC78Aok3tEr0MNwHoRf7vZnga1mQXZ4/Nsodld4VR6Wd+VhfmqnNrsRJyYPfrQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, "node_modules/tsx": { "version": "4.21.0", "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", diff --git a/nodejs/sqlcommenter-nodejs/packages/sqlcommenter-drizzle/package.json b/nodejs/sqlcommenter-nodejs/packages/sqlcommenter-drizzle/package.json index 32a751eb..d63a9454 100644 --- a/nodejs/sqlcommenter-nodejs/packages/sqlcommenter-drizzle/package.json +++ b/nodejs/sqlcommenter-nodejs/packages/sqlcommenter-drizzle/package.json @@ -1,6 +1,6 @@ { "name": "@query-doctor/sqlcommenter-drizzle", - "version": "0.3.0", + "version": "0.4.0", "description": "SQLCommenter patch for drizzle-orm", "main": "dist/cjs/index.js", "type": "module", @@ -28,11 +28,22 @@ "types": "./dist/cjs/http.d.ts", "default": "./dist/cjs/http.js" } + }, + "./fastify": { + "import": { + "types": "./dist/esm/fastify.d.ts", + "default": "./dist/esm/fastify.js" + }, + "require": { + "types": "./dist/cjs/fastify.d.ts", + "default": "./dist/cjs/fastify.js" + } } }, "devDependencies": { "@electric-sql/pglite": "^0.3.10", "@types/node": "^20.19.34", + "fastify": "^5.2.0", "hono": "^4.9.8", "postgres": "^3.4.7", "rewiremock": "^3.14.3", @@ -44,7 +55,13 @@ }, "peerDependencies": { "@opentelemetry/core": ">=2.8.0", - "drizzle-orm": ">=0.35.0" + "drizzle-orm": ">=0.35.0", + "fastify": ">=4.0.0" + }, + "peerDependenciesMeta": { + "fastify": { + "optional": true + } }, "engines": { "node": ">=20.0.0" diff --git a/nodejs/sqlcommenter-nodejs/packages/sqlcommenter-drizzle/src/fastify.ts b/nodejs/sqlcommenter-nodejs/packages/sqlcommenter-drizzle/src/fastify.ts new file mode 100644 index 00000000..3a3b7ce4 --- /dev/null +++ b/nodejs/sqlcommenter-nodejs/packages/sqlcommenter-drizzle/src/fastify.ts @@ -0,0 +1,72 @@ +import type { + FastifyInstance, + FastifyPluginCallback, + FastifyRequest, +} from "fastify"; +import { als } from "./als.js"; +import type { RequestContext } from "./request-context.js"; + +export type SqlcommenterContextFn = ( + request: FastifyRequest, +) => Record; + +export interface SqlcommenterFastifyOptions { + /** + * Extra fields to add to every query's context, merged over the default `route`/`method` + * (e.g. a tenant id, or the matched controller name). Runs once per request in `onRequest`. + */ + context?: SqlcommenterContextFn; +} + +// fastify-plugin sets this symbol so a plugin's hooks/decorators apply to the *parent* scope +// rather than being encapsulated in the plugin's own scope. Our `onRequest` hook has to be +// global so it covers routes — and other plugins' hooks — registered in the parent scope. +// Without it, a plain `register()` silently encapsulates the hook and it tags nothing. This is +// the same primitive `fastify-plugin` relies on, inlined so the integration needs no dependency +// beyond fastify itself. +const SKIP_OVERRIDE = Symbol.for("skip-override"); + +const plugin: FastifyPluginCallback = ( + fastify: FastifyInstance, + options: SqlcommenterFastifyOptions, + done: (err?: Error) => void, +) => { + fastify.addHook("onRequest", (request, _reply, next) => { + const context: RequestContext = { + // `routeOptions.url` is the matched route pattern (e.g. "/items/:id"). It's resolved by the + // time `onRequest` runs; fall back to the raw url for unmatched requests. + route: request.routeOptions?.url ?? request.url, + method: request.method, + ...options.context?.(request), + }; + // Opening the context here — and registering this plugin before any query-issuing plugin — + // means queries from later `onRequest`/`preHandler` hooks and the handler all inherit it, + // not just the handler body. + als.run(context, next); + }); + done(); +}; + +(plugin as unknown as Record)[SKIP_OVERRIDE] = true; + +/** + * Fastify plugin that tags every query issued during a request with its `route` and `method` + * (plus anything returned by `options.context`). + * + * Register it **before** any other plugin whose hooks issue queries (e.g. an auth plugin that + * resolves a session in its own `onRequest`/`preHandler`), so the context is already open when + * those hooks run. Because it hooks `onRequest`, it covers the whole request lifecycle — not + * just the route handler. + * + * @example + * ```ts + * import { sqlcommenterFastify } from "@query-doctor/sqlcommenter-drizzle/fastify"; + * + * await app.register(sqlcommenterFastify); + * await app.register(authPlugin); // queries issued in auth's hooks are tagged too + * ``` + */ +export const sqlcommenterFastify = plugin; +export default sqlcommenterFastify; + +export type { RequestContext } from "./request-context.js"; diff --git a/nodejs/sqlcommenter-nodejs/packages/sqlcommenter-drizzle/src/http.ts b/nodejs/sqlcommenter-nodejs/packages/sqlcommenter-drizzle/src/http.ts index 2efcbb90..5f3a8d9b 100644 --- a/nodejs/sqlcommenter-nodejs/packages/sqlcommenter-drizzle/src/http.ts +++ b/nodejs/sqlcommenter-nodejs/packages/sqlcommenter-drizzle/src/http.ts @@ -2,14 +2,15 @@ import { als } from "./als.js"; import type { RequestContext } from "./request-context.js"; /** - * Wraps the next function in the AsyncLocalStorage with the request context. - * Used to get `route` and `controller` information from the request into the query - * without exposing the underlying AsyncLocalStorage API. + * Runs `next` within an AsyncLocalStorage scope carrying the request context, so any query + * issued during it picks up `route` (and any other provided fields such as `method` and + * `controller`) without exposing the underlying AsyncLocalStorage API. + * + * `next` is invoked for its side effect and its return value is ignored, so the parameter + * accepts any nullary callback — including framework hook callbacks like Fastify's + * `done: (err?: Error) => void` or Express's `next`. */ -export function withRequestContext( - context: RequestContext, - next: () => Promise, -) { +export function withRequestContext(context: RequestContext, next: () => unknown) { als.run(context, next); } diff --git a/nodejs/sqlcommenter-nodejs/packages/sqlcommenter-drizzle/src/index.ts b/nodejs/sqlcommenter-nodejs/packages/sqlcommenter-drizzle/src/index.ts index b01d8948..d12557c5 100644 --- a/nodejs/sqlcommenter-nodejs/packages/sqlcommenter-drizzle/src/index.ts +++ b/nodejs/sqlcommenter-nodejs/packages/sqlcommenter-drizzle/src/index.ts @@ -5,16 +5,27 @@ import { resolveFilePath } from "./path.js"; const LIBRARY_NAME = "sqlcommenter-drizzle"; -type QueryContext = { - queryStack: string[]; -}; - type DriverSession = { prepareQuery: (query: unknown) => unknown }; -// We don't own the Session object here so using a WeakMap to prevent memory leaks -// An alternative could be to set a Symbol in the Session to store the context -// but this approach seems a little bit safer as we avoid interfacing with the object at all -const contexts = new WeakMap(); +// The caller's source location has to be captured when a query is *built* (e.g. `db.select()`), +// because by the time the query actually executes the build-time stack frame is long gone. +// That caller then has to reach the `prepareQuery` patch that writes the comment. +// +// A single `drizzle()` instance shares ONE session object across every query it builds, so the +// caller cannot be keyed by the session: two queries built before either executes would collide +// on the same entry, mis-attributing one `file` tag and dropping the other under any real +// concurrency (e.g. two in-flight HTTP requests, or `Promise.all`). +// +// Instead we tag each built query object with its own caller, and publish that caller into +// `currentCaller` for the *synchronous* window in which that specific query reaches +// `prepareQuery` (`then`/`execute`/`prepare` -> `_prepare` -> `session.prepareQuery` runs with +// no `await` in between). Because the window is synchronous, concurrent queries can never +// interleave inside it, so each query reads exactly its own caller. +let currentCaller: string | undefined; + +// Marks a query object whose `then`/`execute`/`prepare` have already been wrapped, so chained +// rebuilds returning the same object aren't wrapped twice. +const TAGGED = Symbol("sqlcommenter-drizzle.tagged"); function isValidCaller(line: string): boolean { if (line.includes("node_modules")) { @@ -59,22 +70,91 @@ export function traceCaller(): string | undefined { } } -function patchMethod( - target: Function, - thisArg: unknown, - args: any[], - session: DriverSession, -) { - const caller = traceCaller(); - if (caller) { - const ctx = contexts.get(session); - if (ctx) { - ctx.queryStack.push(caller); - } else { - contexts.set(session, { queryStack: [caller] }); +/** + * Wraps `then`/`execute`/`prepare` on a built query so that, while the query synchronously + * reaches `prepareQuery`, its own build-time caller is the one published in `currentCaller`. + */ +function tagExecutable(executable: any, caller: string) { + if (!executable || typeof executable !== "object" || executable[TAGGED]) { + return; + } + executable[TAGGED] = true; + for (const method of ["then", "execute", "prepare"] as const) { + const original = executable[method]; + if (typeof original !== "function") { + continue; } + executable[method] = function (this: unknown, ...args: unknown[]) { + const previous = currentCaller; + currentCaller = caller; + try { + return original.apply(this, args); + } finally { + currentCaller = previous; + } + }; + } +} + +/** + * `db.select()`/`db.insert()`/`db.update()` return an intermediate *builder*; the executable + * query is produced one call later by `.from()`/`.values()`/`.set()`. We wrap the builder in a + * Proxy so that whatever its methods return is run back through `handleResult` and the eventual + * executable gets tagged with the caller captured at build time. + */ +function wrapBuilder(builder: any, caller: string): unknown { + return new Proxy(builder, { + get(target, prop, receiver) { + const value = Reflect.get(target, prop, receiver); + if (typeof value !== "function") { + return value; + } + return function (this: unknown, ...args: unknown[]) { + return handleResult(value.apply(target, args), caller); + }; + }, + }); +} + +function handleResult(result: any, caller: string): unknown { + if (!result || typeof result !== "object") { + return result; + } + // A built, executable query (a drizzle QueryPromise) is thenable — tag it directly. + if (typeof result.then === "function") { + tagExecutable(result, caller); + return result; + } + // Otherwise it's still a builder; keep wrapping until the executable shows up. + return wrapBuilder(result, caller); +} + +/** + * Patches a builder-returning method (`select`/`insert`/`update`/`delete`/relational queries). + * The result is lazy, so we tag it and let `currentCaller` be set when it executes. + */ +function patchBuilderMethod(target: Function, thisArg: unknown, args: any[]) { + const caller = traceCaller(); + const result = Reflect.apply(target, thisArg, args); + return caller ? handleResult(result, caller) : result; +} + +/** + * Patches an eager method (`db.execute()`), which builds *and* reaches `prepareQuery` + * synchronously within this call, so the caller is published around the call itself. + */ +function patchImmediateMethod(target: Function, thisArg: unknown, args: any[]) { + const caller = traceCaller(); + if (!caller) { + return Reflect.apply(target, thisArg, args); + } + const previous = currentCaller; + currentCaller = caller; + try { + return Reflect.apply(target, thisArg, args); + } finally { + currentCaller = previous; } - return Reflect.apply(target, thisArg, args); } const DRIZZLE_ORM_MODE_METHODS = ["findFirst", "findMany"] as const; @@ -112,10 +192,8 @@ export function patchDrizzle( ] as const; if (typeof drizzle.execute === "function") { drizzle.execute = new Proxy(drizzle.execute, { - apply(target, thisArg, args) { - const session = thisArg.session; - return patchMethod(target, thisArg, args, session); - }, + apply: (target, thisArg, args) => + patchImmediateMethod(target, thisArg, args), }); } if (drizzle && "query" in drizzle && drizzle.query) { @@ -126,10 +204,8 @@ export function patchDrizzle( continue; } schema[func] = new Proxy(schema[func], { - apply(target, thisArg, args) { - const session = thisArg.session; - return patchMethod(target, thisArg, args, session); - }, + apply: (target, thisArg, args) => + patchBuilderMethod(target, thisArg, args), }); } } @@ -139,18 +215,13 @@ export function patchDrizzle( if (!drizzle[method] || typeof drizzle[method] !== "function") { continue; } - // patching the CRUD functions. - // the correct function to patch here is QueryPromise.prototype.then - // but because of the way microtasks work, by the time `then` fires, - // the stack is already clear and the caller name is no longer available - // so we have to forcibly get it earlier when the query is built. - // TODO: This isn't 100% reliable as the user could build a query and not run it until much later - // which could throw off this process completely. + // Patching the CRUD entrypoints. The caller is captured here, when the query is built, + // because the build-time stack is the only place the user's call site is still visible — + // by the time the query executes (a microtask later) it's gone. `patchBuilderMethod` tags + // the built query so the caller is reattached for its own synchronous `prepareQuery` window. drizzle[method] = new Proxy(drizzle[method], { - apply(target, thisArg, args) { - const session = thisArg._.session; - return patchMethod(target, thisArg, args, session); - }, + apply: (target, thisArg, args) => + patchBuilderMethod(target, thisArg, args), }); } return drizzle; @@ -162,11 +233,6 @@ const WellKnownFields = { route: "route", } as const; -// This is very non-standard. If `file` is to be a semantic convention this probably -// needs to be discussed with the community. It's what we use at query-doctor so -// sticking with it f -const SQLCOMMENTER_ARRAY_ELEM_DELIMITER = ";"; - /** * Drizzle session is responsible for serializing the query and sending it downstream to * the driver. We're patching `prepareQuery` to add the SQL comments there instead of @@ -183,35 +249,27 @@ function patchSession(session: DriverSession) { } proto.prepareQuery = new Proxy(proto.prepareQuery, { apply(target, thisArg, args) { - try { - const ctx = contexts.get(thisArg); - const requestContext = als.getStore(); - const tags: [string, string][] = [ - [WellKnownFields.dbDriver, "drizzle"], - ]; - // adding traceparent and tracestate - pushW3CTraceContext(tags); - if (ctx) { - tags.push([ - WellKnownFields.file, - // questionable - ctx.queryStack.join(SQLCOMMENTER_ARRAY_ELEM_DELIMITER), - ]); - } - if (args[0]) { - const query = args[0]; - if (!alreadyHasComment(query.sql)) { - if (requestContext) { - for (const key in requestContext) { - tags.push([key, String(requestContext[key])]); - } + // `currentCaller` is set by the built query whose synchronous execution reached this + // call, so it's exactly the caller of the query being prepared. + const caller = currentCaller; + const requestContext = als.getStore(); + const tags: [string, string][] = [[WellKnownFields.dbDriver, "drizzle"]]; + // adding traceparent and tracestate + pushW3CTraceContext(tags); + if (caller) { + tags.push([WellKnownFields.file, caller]); + } + if (args[0]) { + const query = args[0]; + if (!alreadyHasComment(query.sql)) { + if (requestContext) { + for (const key in requestContext) { + tags.push([key, String(requestContext[key])]); } - const sqlComment = serializeTags(tags); - query.sql += sqlComment; } + const sqlComment = serializeTags(tags); + query.sql += sqlComment; } - } finally { - contexts.delete(thisArg); } return Reflect.apply(target, thisArg, args); }, diff --git a/nodejs/sqlcommenter-nodejs/packages/sqlcommenter-drizzle/test/driver-integration.spec.ts b/nodejs/sqlcommenter-nodejs/packages/sqlcommenter-drizzle/test/driver-integration.spec.ts index fa29c8d7..d9b36332 100644 --- a/nodejs/sqlcommenter-nodejs/packages/sqlcommenter-drizzle/test/driver-integration.spec.ts +++ b/nodejs/sqlcommenter-nodejs/packages/sqlcommenter-drizzle/test/driver-integration.spec.ts @@ -28,3 +28,78 @@ test("pglite integration", async () => { assert.fail("Expected an error to be thrown"); } }); + +const watches = pgTable("watches", { + id: serial("id").primaryKey(), + name: text("name"), +}); +const notifs = pgTable("notifs", { + id: serial("id").primaryKey(), + name: text("name"), +}); + +function fileTag(sql: string): string | undefined { + const match = sql.match(/file='([^']*)'/); + return match ? decodeURIComponent(match[1]) : undefined; +} + +async function setupLoggedDb() { + const logged: string[] = []; + const db = patchDrizzle( + drizzle({ + schema: { watches, notifs }, + logger: { logQuery: (query) => logged.push(query) }, + }), + ); + await db.$client.exec( + "CREATE TABLE watches (id serial primary key, name text); CREATE TABLE notifs (id serial primary key, name text);", + ); + return { db, logged }; +} + +// Regression: a single drizzle() instance shares one session across every query, so keying the +// captured caller by the session dropped/clobbered the `file` tag whenever two queries were built +// before either executed (concurrent requests, `Promise.all`). Each query must carry its own tag. +test("concurrent queries each keep their own file tag", async () => { + const { db, logged } = await setupLoggedDb(); + const queryWatches = () => db.select().from(watches); + const queryNotifs = () => db.select().from(notifs); + + await Promise.all([queryWatches(), queryNotifs()]); + + const watchesSql = logged.find((q) => q.includes('from "watches"')); + const notifsSql = logged.find((q) => q.includes('from "notifs"')); + assert.ok(watchesSql, "expected the watches query to be logged"); + assert.ok(notifsSql, "expected the notifs query to be logged"); + + const watchesFile = fileTag(watchesSql); + const notifsFile = fileTag(notifsSql); + assert.ok(watchesFile, "watches query is missing its file tag"); + assert.ok(notifsFile, "notifs query is missing its file tag"); + // Distinct build sites must produce distinct tags — not one joined tag plus a dropped one. + assert.notStrictEqual( + watchesFile, + notifsFile, + "concurrent queries clobbered each other's file tag", + ); + assert.doesNotMatch( + watchesFile!, + /;/, + "file tag must hold a single caller, not a joined stack", + ); +}); + +// Queries can be built in one order and awaited in another; the tag must follow the query object, +// not the order in which queries reach the driver. +test("file tag follows the query even when awaited out of build order", async () => { + const { db, logged } = await setupLoggedDb(); + const built1 = db.select().from(watches); + const built2 = db.select().from(notifs); + await built2; + await built1; + + const watchesFile = fileTag(logged.find((q) => q.includes('from "watches"'))!); + const notifsFile = fileTag(logged.find((q) => q.includes('from "notifs"'))!); + assert.ok(watchesFile && notifsFile, "both queries must keep a file tag"); + assert.notStrictEqual(watchesFile, notifsFile); +}); diff --git a/nodejs/sqlcommenter-nodejs/packages/sqlcommenter-drizzle/test/fastify.spec.ts b/nodejs/sqlcommenter-nodejs/packages/sqlcommenter-drizzle/test/fastify.spec.ts new file mode 100644 index 00000000..63235ec7 --- /dev/null +++ b/nodejs/sqlcommenter-nodejs/packages/sqlcommenter-drizzle/test/fastify.spec.ts @@ -0,0 +1,89 @@ +import { test } from "node:test"; +import assert from "node:assert"; +import Fastify from "fastify"; +import { pgTable, serial, text } from "drizzle-orm/pg-core"; +import { drizzle } from "drizzle-orm/pglite"; +import { patchDrizzle } from "../src/index.js"; +import { sqlcommenterFastify } from "../src/fastify.js"; + +const sessions = pgTable("sessions", { + id: serial("id").primaryKey(), + name: text("name"), +}); +const items = pgTable("items", { + id: serial("id").primaryKey(), + name: text("name"), +}); + +function tag(sql: string, key: string): string | undefined { + const match = sql.match(new RegExp(`${key}='([^']*)'`)); + return match ? decodeURIComponent(match[1]) : undefined; +} + +async function setup() { + const logged: string[] = []; + const db = patchDrizzle( + drizzle({ + schema: { sessions, items }, + logger: { logQuery: (query) => logged.push(query) }, + }), + ); + await db.$client.exec( + "CREATE TABLE sessions(id serial primary key, name text); CREATE TABLE items(id serial primary key, name text);", + ); + return { db, logged }; +} + +// The plugin must (a) apply globally — covering routes registered in the parent scope, which a +// plain encapsulated `register` would silently miss — and (b) cover the whole request lifecycle, +// so a query issued in an auth plugin's own `onRequest` (registered after it) is tagged too, not +// just queries in the route handler. +test("fastify plugin tags lifecycle-wide queries with route and method", async () => { + const { db, logged } = await setup(); + const app = Fastify(); + await app.register(sqlcommenterFastify); + // Auth-style session lookup in its own onRequest hook, registered after the plugin. + app.addHook("onRequest", async () => { + await db.select().from(sessions); + }); + app.get("/items/:id", async () => { + await db.select().from(items); + return { ok: true }; + }); + + const res = await app.inject({ method: "GET", url: "/items/42" }); + await app.close(); + + assert.strictEqual(res.statusCode, 200); + const sessionSql = logged.find((q) => q.includes('from "sessions"')); + const itemSql = logged.find((q) => q.includes('from "items"')); + assert.ok(sessionSql, "expected the auth onRequest query to run"); + assert.ok(itemSql, "expected the handler query to run"); + + // onRequest-phase query (the case a handler wrap misses). + assert.strictEqual(tag(sessionSql, "route"), "/items/:id"); + assert.strictEqual(tag(sessionSql, "method"), "GET"); + // handler-phase query. + assert.strictEqual(tag(itemSql, "route"), "/items/:id"); + assert.strictEqual(tag(itemSql, "method"), "GET"); +}); + +test("fastify plugin merges extra context fields", async () => { + const { db, logged } = await setup(); + const app = Fastify(); + await app.register(sqlcommenterFastify, { + context: (request) => ({ controller: "items", host: request.headers.host }), + }); + app.get("/items/:id", async () => { + await db.select().from(items); + return { ok: true }; + }); + + await app.inject({ method: "GET", url: "/items/42", headers: { host: "x" } }); + await app.close(); + + const itemSql = logged.find((q) => q.includes('from "items"'))!; + assert.strictEqual(tag(itemSql, "route"), "/items/:id"); + assert.strictEqual(tag(itemSql, "controller"), "items"); + assert.strictEqual(tag(itemSql, "host"), "x"); +}); diff --git a/nodejs/sqlcommenter-nodejs/packages/sqlcommenter-mikroorm/README.md b/nodejs/sqlcommenter-nodejs/packages/sqlcommenter-mikroorm/README.md index 414ae777..7255660c 100644 --- a/nodejs/sqlcommenter-nodejs/packages/sqlcommenter-mikroorm/README.md +++ b/nodejs/sqlcommenter-nodejs/packages/sqlcommenter-mikroorm/README.md @@ -82,18 +82,57 @@ app.use((c, next) => { #### Fastify +The recommended way is the first-party plugin, which wires everything correctly: + +```ts +import { sqlcommenterFastify } from "@query-doctor/sqlcommenter-mikroorm/fastify"; + +// Register it BEFORE any plugin whose hooks issue queries (e.g. auth), so the context +// is already open when those hooks run. +await app.register(sqlcommenterFastify); +await app.register(authPlugin); +``` + +It hooks `onRequest`, so it tags queries from the **entire request lifecycle** — including queries +issued in other plugins' `onRequest`/`preHandler` hooks — not just the route handler. Pass +`context` to add extra fields: + +```ts +await app.register(sqlcommenterFastify, { + context: (request) => ({ controller: "items" }), +}); +``` + +##### Doing it manually + +If you'd rather wire it yourself with `withRequestContext`, two things are easy to get wrong: + +- **Register the hook globally with [`fastify-plugin`](https://github.com/fastify/fastify-plugin).** + A plain `app.register(plugin)` encapsulates the `onRequest` hook, so it silently does **not** + apply to routes registered in the parent scope — you get no `route`/`method` tags and no error. +- **Hook `onRequest` (not the route handler), registered before any plugin whose hooks issue + queries** (e.g. an auth plugin that resolves a session in its own `onRequest`/`preHandler`). + Wrapping only the handler misses those earlier queries. + ```ts +import fp from "fastify-plugin"; import { withRequestContext } from "@query-doctor/sqlcommenter-mikroorm/http"; -app.addHook("onRequest", (request, _, done) => { - withRequestContext( - { - route: request.routerPath, - method: request.method, - }, - done - ); +const sqlcommenter = fp((app, _opts, done) => { + app.addHook("onRequest", (request, _reply, next) => { + withRequestContext( + { + // `routerPath` was removed in Fastify v5; `routeOptions.url` is the matched route pattern. + route: request.routeOptions?.url ?? request.url, + method: request.method, + }, + next + ); + }); + done(); }); + +await app.register(sqlcommenter); // before auth, etc. ``` #### NestJS diff --git a/nodejs/sqlcommenter-nodejs/packages/sqlcommenter-mikroorm/package-lock.json b/nodejs/sqlcommenter-nodejs/packages/sqlcommenter-mikroorm/package-lock.json index d61a516e..7ba26f54 100644 --- a/nodejs/sqlcommenter-nodejs/packages/sqlcommenter-mikroorm/package-lock.json +++ b/nodejs/sqlcommenter-nodejs/packages/sqlcommenter-mikroorm/package-lock.json @@ -1,12 +1,12 @@ { "name": "@query-doctor/sqlcommenter-mikroorm", - "version": "0.2.0", + "version": "0.3.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@query-doctor/sqlcommenter-mikroorm", - "version": "0.2.0", + "version": "0.3.0", "license": "Apache-2.0", "dependencies": { "@opentelemetry/api": "~1.9.0" @@ -15,6 +15,7 @@ "@mikro-orm/better-sqlite": "^6.6.7", "@types/node": "^20.19.34", "better-sqlite3": "^12.6.2", + "fastify": "^5.2.0", "tsx": "^4.20.5", "typescript": "^5.9.3" }, @@ -23,7 +24,13 @@ }, "peerDependencies": { "@mikro-orm/core": ">=6.4.0", - "@opentelemetry/core": ">=2.8.0" + "@opentelemetry/core": ">=2.8.0", + "fastify": ">=4.0.0" + }, + "peerDependenciesMeta": { + "fastify": { + "optional": true + } } }, "node_modules/@esbuild/aix-ppc64": { @@ -468,6 +475,123 @@ "node": ">=18" } }, + "node_modules/@fastify/ajv-compiler": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@fastify/ajv-compiler/-/ajv-compiler-4.0.5.tgz", + "integrity": "sha512-KoWKW+MhvfTRWL4qrhUwAAZoaChluo0m0vbiJlGMt2GXvL4LVPQEjt8kSpHI3IBq5Rez8fg+XeH3cneztq+C7A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "ajv": "^8.12.0", + "ajv-formats": "^3.0.1", + "fast-uri": "^3.0.0" + } + }, + "node_modules/@fastify/error": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@fastify/error/-/error-4.2.0.tgz", + "integrity": "sha512-RSo3sVDXfHskiBZKBPRgnQTtIqpi/7zhJOEmAxCiBcM7d0uwdGdxLlsCaLzGs8v8NnxIRlfG0N51p5yFaOentQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/@fastify/fast-json-stringify-compiler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@fastify/fast-json-stringify-compiler/-/fast-json-stringify-compiler-5.0.3.tgz", + "integrity": "sha512-uik7yYHkLr6fxd8hJSZ8c+xF4WafPK+XzneQDPU+D10r5X19GW8lJcom2YijX2+qtFF1ENJlHXKFM9ouXNJYgQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "fast-json-stringify": "^6.0.0" + } + }, + "node_modules/@fastify/forwarded": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@fastify/forwarded/-/forwarded-3.0.1.tgz", + "integrity": "sha512-JqDochHFqXs3C3Ml3gOY58zM7OqO9ENqPo0UqAjAjH8L01fRZqwX9iLeX34//kiJubF7r2ZQHtBRU36vONbLlw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/@fastify/merge-json-schemas": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@fastify/merge-json-schemas/-/merge-json-schemas-0.2.1.tgz", + "integrity": "sha512-OA3KGBCy6KtIvLf8DINC5880o5iBlDX4SxzLQS8HorJAbqluzLRn80UXU0bxZn7UOFhFgpRJDasfwn9nG4FG4A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/@fastify/proxy-addr": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@fastify/proxy-addr/-/proxy-addr-5.1.0.tgz", + "integrity": "sha512-INS+6gh91cLUjB+PVHfu1UqcB76Sqtpyp7bnL+FYojhjygvOPA9ctiD/JDKsyD9Xgu4hUhCSJBPig/w7duNajw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/forwarded": "^3.0.0", + "ipaddr.js": "^2.1.0" + } + }, "node_modules/@mikro-orm/better-sqlite": { "version": "6.6.7", "resolved": "https://registry.npmjs.org/@mikro-orm/better-sqlite/-/better-sqlite-6.6.7.tgz", @@ -626,6 +750,13 @@ "node": ">=14" } }, + "node_modules/@pinojs/redact": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz", + "integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { "version": "20.19.34", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.34.tgz", @@ -636,6 +767,48 @@ "undici-types": "~6.21.0" } }, + "node_modules/abstract-logging": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz", + "integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==", + "dev": true, + "license": "MIT" + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, "node_modules/array-union": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", @@ -646,6 +819,37 @@ "node": ">=8" } }, + "node_modules/atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/avvio": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/avvio/-/avvio-9.2.0.tgz", + "integrity": "sha512-2t/sy01ArdHHE0vRH5Hsay+RtCZt3dLPji7W7/MMOCEgze5b7SNDC4j5H6FnVgPkI1MTNFGzHdHrVXDDl7QSSQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/error": "^4.0.0", + "fastq": "^1.17.1" + } + }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -766,6 +970,20 @@ "node": ">=14" } }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/dataloader": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/dataloader/-/dataloader-2.2.3.tgz", @@ -817,6 +1035,16 @@ "node": ">=4.0.0" } }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -949,6 +1177,20 @@ "node": ">=6" } }, + "node_modules/fast-decode-uri-component": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz", + "integrity": "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, "node_modules/fast-glob": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", @@ -966,12 +1208,97 @@ "node": ">=8.6.0" } }, + "node_modules/fast-json-stringify": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/fast-json-stringify/-/fast-json-stringify-6.4.0.tgz", + "integrity": "sha512-ibRCQ0GZKJIQ+P3Et1h0LhPgp3PMTYk0MH8O+kW3lNYsvmaQww5Nn3f1jf73Q0jR1Yz3a1CDP4/NZD3vOajWJQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/merge-json-schemas": "^0.2.0", + "ajv": "^8.12.0", + "ajv-formats": "^3.0.1", + "fast-uri": "^3.0.0", + "json-schema-ref-resolver": "^3.0.0", + "rfdc": "^1.2.0" + } + }, + "node_modules/fast-querystring": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/fast-querystring/-/fast-querystring-1.1.2.tgz", + "integrity": "sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-decode-uri-component": "^1.0.1" + } + }, + "node_modules/fast-uri": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.3.tgz", + "integrity": "sha512-i70LwGWUduXqzicKXWshooq+sWL1K3WUU5rKZNG/0i3a1OSoX3HqhH5WbWwTmqWfor4urUakGPiRQcleRZTwOg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fastify": { + "version": "5.9.0", + "resolved": "https://registry.npmjs.org/fastify/-/fastify-5.9.0.tgz", + "integrity": "sha512-VMS5lE0zj+MZlJpQa3Qv5iGjfun0H2N7VRgoBwpcTNQ2bdIQpv7fDpb+HGteGbicBsGkzGS+X+hdx9mmrfWuHQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/ajv-compiler": "^4.0.5", + "@fastify/error": "^4.0.0", + "@fastify/fast-json-stringify-compiler": "^5.0.0", + "@fastify/proxy-addr": "^5.0.0", + "abstract-logging": "^2.0.1", + "avvio": "^9.0.0", + "fast-json-stringify": "^6.0.0", + "find-my-way": "^9.6.0", + "light-my-request": "^6.0.0", + "pino": "^9.14.0 || ^10.1.0", + "process-warning": "^5.0.0", + "rfdc": "^1.3.1", + "secure-json-parse": "^4.0.0", + "semver": "^7.6.0", + "toad-cache": "^3.7.0" + } + }, "node_modules/fastq": { "version": "1.20.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", "license": "ISC", - "peer": true, "dependencies": { "reusify": "^1.0.4" } @@ -996,6 +1323,21 @@ "node": ">=8" } }, + "node_modules/find-my-way": { + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-9.6.0.tgz", + "integrity": "sha512-Zf4Xve4RymLl7NgaavNebZ01joJ8MfVerOG43wy7SHLO+r+K0C6d/SE0BiR7AV5V1VOCFlOP7ecdo+I4qmiHrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-querystring": "^1.0.0", + "safe-regex2": "^5.0.0" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/fs-constants": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", @@ -1187,6 +1529,16 @@ "node": ">= 0.10" } }, + "node_modules/ipaddr.js": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.4.0.tgz", + "integrity": "sha512-9VGk3HGanVE6JoZXHiCpnGy5X0jYDnN4EA4lntFPj+1vIWlFhIylq2CrrCOJH9EAhc5CYhq18F2Av2tgoAPsYQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, "node_modules/is-core-module": { "version": "2.16.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", @@ -1236,6 +1588,33 @@ "node": ">=0.12.0" } }, + "node_modules/json-schema-ref-resolver": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/json-schema-ref-resolver/-/json-schema-ref-resolver-3.0.0.tgz", + "integrity": "sha512-hOrZIVL5jyYFjzk7+y7n5JDzGlU8rfWDuYyHwGa2WA8/pcmMHezp2xsVwxrebD/Q9t8Nc5DboieySDpCp4WG4A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, "node_modules/jsonfile": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", @@ -1300,6 +1679,45 @@ } } }, + "node_modules/light-my-request": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/light-my-request/-/light-my-request-6.6.0.tgz", + "integrity": "sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause", + "dependencies": { + "cookie": "^1.0.1", + "process-warning": "^4.0.0", + "set-cookie-parser": "^2.6.0" + } + }, + "node_modules/light-my-request/node_modules/process-warning": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-4.0.1.tgz", + "integrity": "sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, "node_modules/lodash": { "version": "4.17.23", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", @@ -1398,6 +1816,16 @@ "node": ">=10" } }, + "node_modules/on-exit-leak-free": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -1445,6 +1873,46 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pino": { + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/pino/-/pino-10.3.1.tgz", + "integrity": "sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pinojs/redact": "^0.4.0", + "atomic-sleep": "^1.0.0", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^3.0.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^5.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^4.0.1", + "thread-stream": "^4.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/pino-abstract-transport": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-3.0.0.tgz", + "integrity": "sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "split2": "^4.0.0" + } + }, + "node_modules/pino-std-serializers": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.1.0.tgz", + "integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==", + "dev": true, + "license": "MIT" + }, "node_modules/prebuild-install": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", @@ -1473,6 +1941,23 @@ "node": ">=10" } }, + "node_modules/process-warning": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz", + "integrity": "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, "node_modules/pump": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", @@ -1505,6 +1990,13 @@ "license": "MIT", "peer": true }, + "node_modules/quick-format-unescaped": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", + "dev": true, + "license": "MIT" + }, "node_modules/rc": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", @@ -1536,6 +2028,16 @@ "node": ">= 6" } }, + "node_modules/real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12.13.0" + } + }, "node_modules/rechoir": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", @@ -1556,6 +2058,16 @@ "license": "Apache-2.0", "peer": true }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/resolve": { "version": "1.22.11", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", @@ -1597,17 +2109,33 @@ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } }, + "node_modules/ret": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.5.0.tgz", + "integrity": "sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/reusify": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", "license": "MIT", - "peer": true, "engines": { "iojs": ">=1.0.0", "node": ">=0.10.0" } }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "dev": true, + "license": "MIT" + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -1653,6 +2181,56 @@ ], "license": "MIT" }, + "node_modules/safe-regex2": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/safe-regex2/-/safe-regex2-5.1.1.tgz", + "integrity": "sha512-mOSBvHGDZMuIEZMdOz/aCEYDCv0E7nfcNsIhUF+/P+xC7Hyf3FkvymqgPbg9D1EdSGu+uKbJgy09K/RKKc7kJA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "ret": "~0.5.0" + }, + "bin": { + "safe-regex2": "bin/safe-regex2.js" + } + }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/secure-json-parse": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-4.1.0.tgz", + "integrity": "sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/semver": { "version": "7.7.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", @@ -1666,6 +2244,13 @@ "node": ">=10" } }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "dev": true, + "license": "MIT" + }, "node_modules/simple-concat": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", @@ -1723,6 +2308,26 @@ "node": ">=8" } }, + "node_modules/sonic-boom": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz", + "integrity": "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, "node_modules/sqlstring": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz", @@ -1816,6 +2421,26 @@ "node": ">=8.0.0" } }, + "node_modules/thread-stream": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-4.2.0.tgz", + "integrity": "sha512-e2zZ96wSChazBsbENf/Pcm/4swHt2cEKQ92rhUjkL9GCKiTDJIaTBenjE/m9DXi0QBmTMDkFDdOomUy20A1tDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "real-require": "^1.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/thread-stream/node_modules/real-require": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-1.0.0.tgz", + "integrity": "sha512-P4nbQYQfePJxRSmY+v/KINxVucm4NF3p3s7pJveMTtom52FR4YGltUQLB8idDXwDDWW+eYrWDFbuzUnjoWHF7g==", + "dev": true, + "license": "MIT" + }, "node_modules/tildify": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/tildify/-/tildify-2.0.0.tgz", @@ -1839,6 +2464,16 @@ "node": ">=8.0" } }, + "node_modules/toad-cache": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/toad-cache/-/toad-cache-3.7.1.tgz", + "integrity": "sha512-5DXWzE4Vz7xNHsv+xQ+MGfJYyC78Aok3tEr0MNwHoRf7vZnga1mQXZ4/Nsodld4VR6Wd+VhfmqnNrsRJyYPfrQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, "node_modules/tsx": { "version": "4.21.0", "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", diff --git a/nodejs/sqlcommenter-nodejs/packages/sqlcommenter-mikroorm/package.json b/nodejs/sqlcommenter-nodejs/packages/sqlcommenter-mikroorm/package.json index 4aa9d31b..fdd78ba3 100644 --- a/nodejs/sqlcommenter-nodejs/packages/sqlcommenter-mikroorm/package.json +++ b/nodejs/sqlcommenter-nodejs/packages/sqlcommenter-mikroorm/package.json @@ -1,6 +1,6 @@ { "name": "@query-doctor/sqlcommenter-mikroorm", - "version": "0.2.0", + "version": "0.3.0", "description": "SQLCommenter patch for MikroORM", "main": "dist/cjs/index.js", "type": "module", @@ -28,12 +28,23 @@ "types": "./dist/cjs/http.d.ts", "default": "./dist/cjs/http.js" } + }, + "./fastify": { + "import": { + "types": "./dist/esm/fastify.d.ts", + "default": "./dist/esm/fastify.js" + }, + "require": { + "types": "./dist/cjs/fastify.d.ts", + "default": "./dist/cjs/fastify.js" + } } }, "devDependencies": { "@mikro-orm/better-sqlite": "^6.6.7", "@types/node": "^20.19.34", "better-sqlite3": "^12.6.2", + "fastify": "^5.2.0", "tsx": "^4.20.5", "typescript": "^5.9.3" }, @@ -42,7 +53,13 @@ }, "peerDependencies": { "@mikro-orm/core": ">=6.4.0", - "@opentelemetry/core": ">=2.8.0" + "@opentelemetry/core": ">=2.8.0", + "fastify": ">=4.0.0" + }, + "peerDependenciesMeta": { + "fastify": { + "optional": true + } }, "engines": { "node": ">=20.0.0" diff --git a/nodejs/sqlcommenter-nodejs/packages/sqlcommenter-mikroorm/src/fastify.ts b/nodejs/sqlcommenter-nodejs/packages/sqlcommenter-mikroorm/src/fastify.ts new file mode 100644 index 00000000..94dacca9 --- /dev/null +++ b/nodejs/sqlcommenter-nodejs/packages/sqlcommenter-mikroorm/src/fastify.ts @@ -0,0 +1,72 @@ +import type { + FastifyInstance, + FastifyPluginCallback, + FastifyRequest, +} from "fastify"; +import { als } from "./als.js"; +import type { RequestContext } from "./request-context.js"; + +export type SqlcommenterContextFn = ( + request: FastifyRequest, +) => Record; + +export interface SqlcommenterFastifyOptions { + /** + * Extra fields to add to every query's context, merged over the default `route`/`method` + * (e.g. a tenant id, or the matched controller name). Runs once per request in `onRequest`. + */ + context?: SqlcommenterContextFn; +} + +// fastify-plugin sets this symbol so a plugin's hooks/decorators apply to the *parent* scope +// rather than being encapsulated in the plugin's own scope. Our `onRequest` hook has to be +// global so it covers routes — and other plugins' hooks — registered in the parent scope. +// Without it, a plain `register()` silently encapsulates the hook and it tags nothing. This is +// the same primitive `fastify-plugin` relies on, inlined so the integration needs no dependency +// beyond fastify itself. +const SKIP_OVERRIDE = Symbol.for("skip-override"); + +const plugin: FastifyPluginCallback = ( + fastify: FastifyInstance, + options: SqlcommenterFastifyOptions, + done: (err?: Error) => void, +) => { + fastify.addHook("onRequest", (request, _reply, next) => { + const context: RequestContext = { + // `routeOptions.url` is the matched route pattern (e.g. "/items/:id"). It's resolved by the + // time `onRequest` runs; fall back to the raw url for unmatched requests. + route: request.routeOptions?.url ?? request.url, + method: request.method, + ...options.context?.(request), + }; + // Opening the context here — and registering this plugin before any query-issuing plugin — + // means queries from later `onRequest`/`preHandler` hooks and the handler all inherit it, + // not just the handler body. + als.run(context, next); + }); + done(); +}; + +(plugin as unknown as Record)[SKIP_OVERRIDE] = true; + +/** + * Fastify plugin that tags every query issued during a request with its `route` and `method` + * (plus anything returned by `options.context`). + * + * Register it **before** any other plugin whose hooks issue queries (e.g. an auth plugin that + * resolves a session in its own `onRequest`/`preHandler`), so the context is already open when + * those hooks run. Because it hooks `onRequest`, it covers the whole request lifecycle — not + * just the route handler. + * + * @example + * ```ts + * import { sqlcommenterFastify } from "@query-doctor/sqlcommenter-mikroorm/fastify"; + * + * await app.register(sqlcommenterFastify); + * await app.register(authPlugin); // queries issued in auth's hooks are tagged too + * ``` + */ +export const sqlcommenterFastify = plugin; +export default sqlcommenterFastify; + +export type { RequestContext } from "./request-context.js"; diff --git a/nodejs/sqlcommenter-nodejs/packages/sqlcommenter-mikroorm/src/http.ts b/nodejs/sqlcommenter-nodejs/packages/sqlcommenter-mikroorm/src/http.ts index 2efcbb90..5f3a8d9b 100644 --- a/nodejs/sqlcommenter-nodejs/packages/sqlcommenter-mikroorm/src/http.ts +++ b/nodejs/sqlcommenter-nodejs/packages/sqlcommenter-mikroorm/src/http.ts @@ -2,14 +2,15 @@ import { als } from "./als.js"; import type { RequestContext } from "./request-context.js"; /** - * Wraps the next function in the AsyncLocalStorage with the request context. - * Used to get `route` and `controller` information from the request into the query - * without exposing the underlying AsyncLocalStorage API. + * Runs `next` within an AsyncLocalStorage scope carrying the request context, so any query + * issued during it picks up `route` (and any other provided fields such as `method` and + * `controller`) without exposing the underlying AsyncLocalStorage API. + * + * `next` is invoked for its side effect and its return value is ignored, so the parameter + * accepts any nullary callback — including framework hook callbacks like Fastify's + * `done: (err?: Error) => void` or Express's `next`. */ -export function withRequestContext( - context: RequestContext, - next: () => Promise, -) { +export function withRequestContext(context: RequestContext, next: () => unknown) { als.run(context, next); } diff --git a/nodejs/sqlcommenter-nodejs/packages/sqlcommenter-mikroorm/test/fastify.spec.ts b/nodejs/sqlcommenter-nodejs/packages/sqlcommenter-mikroorm/test/fastify.spec.ts new file mode 100644 index 00000000..87e80b47 --- /dev/null +++ b/nodejs/sqlcommenter-nodejs/packages/sqlcommenter-mikroorm/test/fastify.spec.ts @@ -0,0 +1,56 @@ +import { test } from "node:test"; +import assert from "node:assert"; +import Fastify from "fastify"; +import { sqlcommenterFastify } from "../src/fastify.js"; +import { als } from "../src/als.js"; + +// The plugin is driver-agnostic — it only opens the shared AsyncLocalStorage context that the +// query patch reads. These tests assert that context directly, so they don't need a real ORM. + +// It must apply globally (covering parent-scope routes a plain encapsulated register would miss) +// and cover the whole lifecycle, so an onRequest hook registered after it still sees the context. +test("fastify plugin exposes route/method context lifecycle-wide", async () => { + const seen: Record[] = []; + const app = Fastify(); + await app.register(sqlcommenterFastify); + app.addHook("onRequest", async () => { + const store = als.getStore(); + if (store) seen.push({ phase: "onRequest", ...store }); + }); + app.get("/items/:id", async () => { + const store = als.getStore(); + if (store) seen.push({ phase: "handler", ...store }); + return { ok: true }; + }); + + const res = await app.inject({ method: "GET", url: "/items/42" }); + await app.close(); + + assert.strictEqual(res.statusCode, 200); + assert.deepStrictEqual(seen, [ + { phase: "onRequest", route: "/items/:id", method: "GET" }, + { phase: "handler", route: "/items/:id", method: "GET" }, + ]); +}); + +test("fastify plugin merges extra context fields", async () => { + let store: Record | undefined; + const app = Fastify(); + await app.register(sqlcommenterFastify, { + context: (request) => ({ controller: "items", host: request.headers.host }), + }); + app.get("/items/:id", async () => { + store = als.getStore(); + return { ok: true }; + }); + + await app.inject({ method: "GET", url: "/items/42", headers: { host: "x" } }); + await app.close(); + + assert.deepStrictEqual(store, { + route: "/items/:id", + method: "GET", + controller: "items", + host: "x", + }); +}); diff --git a/nodejs/sqlcommenter-nodejs/packages/sqlcommenter-typeorm/README.md b/nodejs/sqlcommenter-nodejs/packages/sqlcommenter-typeorm/README.md index 19f0e35c..57a5db01 100644 --- a/nodejs/sqlcommenter-nodejs/packages/sqlcommenter-typeorm/README.md +++ b/nodejs/sqlcommenter-nodejs/packages/sqlcommenter-typeorm/README.md @@ -81,18 +81,57 @@ app.use((c, next) => { #### Fastify +The recommended way is the first-party plugin, which wires everything correctly: + +```ts +import { sqlcommenterFastify } from "@query-doctor/sqlcommenter-typeorm/fastify"; + +// Register it BEFORE any plugin whose hooks issue queries (e.g. auth), so the context +// is already open when those hooks run. +await app.register(sqlcommenterFastify); +await app.register(authPlugin); +``` + +It hooks `onRequest`, so it tags queries from the **entire request lifecycle** — including queries +issued in other plugins' `onRequest`/`preHandler` hooks — not just the route handler. Pass +`context` to add extra fields: + +```ts +await app.register(sqlcommenterFastify, { + context: (request) => ({ controller: "items" }), +}); +``` + +##### Doing it manually + +If you'd rather wire it yourself with `withRequestContext`, two things are easy to get wrong: + +- **Register the hook globally with [`fastify-plugin`](https://github.com/fastify/fastify-plugin).** + A plain `app.register(plugin)` encapsulates the `onRequest` hook, so it silently does **not** + apply to routes registered in the parent scope — you get no `route`/`method` tags and no error. +- **Hook `onRequest` (not the route handler), registered before any plugin whose hooks issue + queries** (e.g. an auth plugin that resolves a session in its own `onRequest`/`preHandler`). + Wrapping only the handler misses those earlier queries. + ```ts +import fp from "fastify-plugin"; import { withRequestContext } from "@query-doctor/sqlcommenter-typeorm/http"; -app.addHook("onRequest", (request, _, done) => { - withRequestContext( - { - route: request.routerPath, - method: request.method, - }, - done - ); +const sqlcommenter = fp((app, _opts, done) => { + app.addHook("onRequest", (request, _reply, next) => { + withRequestContext( + { + // `routerPath` was removed in Fastify v5; `routeOptions.url` is the matched route pattern. + route: request.routeOptions?.url ?? request.url, + method: request.method, + }, + next + ); + }); + done(); }); + +await app.register(sqlcommenter); // before auth, etc. ``` #### NestJS diff --git a/nodejs/sqlcommenter-nodejs/packages/sqlcommenter-typeorm/package-lock.json b/nodejs/sqlcommenter-nodejs/packages/sqlcommenter-typeorm/package-lock.json index 4181132b..d50dc7f9 100644 --- a/nodejs/sqlcommenter-nodejs/packages/sqlcommenter-typeorm/package-lock.json +++ b/nodejs/sqlcommenter-nodejs/packages/sqlcommenter-typeorm/package-lock.json @@ -1,18 +1,19 @@ { "name": "@query-doctor/sqlcommenter-typeorm", - "version": "0.2.0", + "version": "0.3.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@query-doctor/sqlcommenter-typeorm", - "version": "0.2.0", + "version": "0.3.0", "license": "Apache-2.0", "dependencies": { "@opentelemetry/api": "~1.9.0" }, "devDependencies": { "@types/node": "^20.19.34", + "fastify": "^5.2.0", "reflect-metadata": "^0.2.2", "sql.js": "^1.12.0", "tsx": "^4.20.5", @@ -23,7 +24,13 @@ }, "peerDependencies": { "@opentelemetry/core": ">=2.8.0", + "fastify": ">=4.0.0", "typeorm": ">=0.3.0" + }, + "peerDependenciesMeta": { + "fastify": { + "optional": true + } } }, "node_modules/@esbuild/aix-ppc64": { @@ -468,6 +475,123 @@ "node": ">=18" } }, + "node_modules/@fastify/ajv-compiler": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@fastify/ajv-compiler/-/ajv-compiler-4.0.5.tgz", + "integrity": "sha512-KoWKW+MhvfTRWL4qrhUwAAZoaChluo0m0vbiJlGMt2GXvL4LVPQEjt8kSpHI3IBq5Rez8fg+XeH3cneztq+C7A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "ajv": "^8.12.0", + "ajv-formats": "^3.0.1", + "fast-uri": "^3.0.0" + } + }, + "node_modules/@fastify/error": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@fastify/error/-/error-4.2.0.tgz", + "integrity": "sha512-RSo3sVDXfHskiBZKBPRgnQTtIqpi/7zhJOEmAxCiBcM7d0uwdGdxLlsCaLzGs8v8NnxIRlfG0N51p5yFaOentQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/@fastify/fast-json-stringify-compiler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@fastify/fast-json-stringify-compiler/-/fast-json-stringify-compiler-5.0.3.tgz", + "integrity": "sha512-uik7yYHkLr6fxd8hJSZ8c+xF4WafPK+XzneQDPU+D10r5X19GW8lJcom2YijX2+qtFF1ENJlHXKFM9ouXNJYgQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "fast-json-stringify": "^6.0.0" + } + }, + "node_modules/@fastify/forwarded": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@fastify/forwarded/-/forwarded-3.0.1.tgz", + "integrity": "sha512-JqDochHFqXs3C3Ml3gOY58zM7OqO9ENqPo0UqAjAjH8L01fRZqwX9iLeX34//kiJubF7r2ZQHtBRU36vONbLlw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/@fastify/merge-json-schemas": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@fastify/merge-json-schemas/-/merge-json-schemas-0.2.1.tgz", + "integrity": "sha512-OA3KGBCy6KtIvLf8DINC5880o5iBlDX4SxzLQS8HorJAbqluzLRn80UXU0bxZn7UOFhFgpRJDasfwn9nG4FG4A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/@fastify/proxy-addr": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@fastify/proxy-addr/-/proxy-addr-5.1.0.tgz", + "integrity": "sha512-INS+6gh91cLUjB+PVHfu1UqcB76Sqtpyp7bnL+FYojhjygvOPA9ctiD/JDKsyD9Xgu4hUhCSJBPig/w7duNajw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/forwarded": "^3.0.0", + "ipaddr.js": "^2.1.0" + } + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -521,6 +645,13 @@ "node": ">=14" } }, + "node_modules/@pinojs/redact": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz", + "integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==", + "dev": true, + "license": "MIT" + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -549,6 +680,48 @@ "undici-types": "~6.21.0" } }, + "node_modules/abstract-logging": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz", + "integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==", + "dev": true, + "license": "MIT" + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, "node_modules/ansi-regex": { "version": "6.2.2", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", @@ -595,6 +768,16 @@ "node": ">= 6.0.0" } }, + "node_modules/atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/available-typed-arrays": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", @@ -611,6 +794,27 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/avvio": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/avvio/-/avvio-9.2.0.tgz", + "integrity": "sha512-2t/sy01ArdHHE0vRH5Hsay+RtCZt3dLPji7W7/MMOCEgze5b7SNDC4j5H6FnVgPkI1MTNFGzHdHrVXDDl7QSSQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/error": "^4.0.0", + "fastq": "^1.17.1" + } + }, "node_modules/balanced-match": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", @@ -844,6 +1048,20 @@ "license": "MIT", "peer": true }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -917,6 +1135,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/dotenv": { "version": "16.6.1", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", @@ -1044,6 +1272,131 @@ "node": ">=6" } }, + "node_modules/fast-decode-uri-component": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz", + "integrity": "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stringify": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/fast-json-stringify/-/fast-json-stringify-6.4.0.tgz", + "integrity": "sha512-ibRCQ0GZKJIQ+P3Et1h0LhPgp3PMTYk0MH8O+kW3lNYsvmaQww5Nn3f1jf73Q0jR1Yz3a1CDP4/NZD3vOajWJQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/merge-json-schemas": "^0.2.0", + "ajv": "^8.12.0", + "ajv-formats": "^3.0.1", + "fast-uri": "^3.0.0", + "json-schema-ref-resolver": "^3.0.0", + "rfdc": "^1.2.0" + } + }, + "node_modules/fast-querystring": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/fast-querystring/-/fast-querystring-1.1.2.tgz", + "integrity": "sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-decode-uri-component": "^1.0.1" + } + }, + "node_modules/fast-uri": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.3.tgz", + "integrity": "sha512-i70LwGWUduXqzicKXWshooq+sWL1K3WUU5rKZNG/0i3a1OSoX3HqhH5WbWwTmqWfor4urUakGPiRQcleRZTwOg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fastify": { + "version": "5.9.0", + "resolved": "https://registry.npmjs.org/fastify/-/fastify-5.9.0.tgz", + "integrity": "sha512-VMS5lE0zj+MZlJpQa3Qv5iGjfun0H2N7VRgoBwpcTNQ2bdIQpv7fDpb+HGteGbicBsGkzGS+X+hdx9mmrfWuHQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/ajv-compiler": "^4.0.5", + "@fastify/error": "^4.0.0", + "@fastify/fast-json-stringify-compiler": "^5.0.0", + "@fastify/proxy-addr": "^5.0.0", + "abstract-logging": "^2.0.1", + "avvio": "^9.0.0", + "fast-json-stringify": "^6.0.0", + "find-my-way": "^9.6.0", + "light-my-request": "^6.0.0", + "pino": "^9.14.0 || ^10.1.0", + "process-warning": "^5.0.0", + "rfdc": "^1.3.1", + "secure-json-parse": "^4.0.0", + "semver": "^7.6.0", + "toad-cache": "^3.7.0" + } + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/find-my-way": { + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-9.6.0.tgz", + "integrity": "sha512-Zf4Xve4RymLl7NgaavNebZ01joJ8MfVerOG43wy7SHLO+r+K0C6d/SE0BiR7AV5V1VOCFlOP7ecdo+I4qmiHrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-querystring": "^1.0.0", + "safe-regex2": "^5.0.0" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/for-each": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", @@ -1282,6 +1635,16 @@ "license": "ISC", "peer": true }, + "node_modules/ipaddr.js": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.4.0.tgz", + "integrity": "sha512-9VGk3HGanVE6JoZXHiCpnGy5X0jYDnN4EA4lntFPj+1vIWlFhIylq2CrrCOJH9EAhc5CYhq18F2Av2tgoAPsYQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, "node_modules/is-callable": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", @@ -1351,6 +1714,72 @@ "@pkgjs/parseargs": "^0.11.0" } }, + "node_modules/json-schema-ref-resolver": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/json-schema-ref-resolver/-/json-schema-ref-resolver-3.0.0.tgz", + "integrity": "sha512-hOrZIVL5jyYFjzk7+y7n5JDzGlU8rfWDuYyHwGa2WA8/pcmMHezp2xsVwxrebD/Q9t8Nc5DboieySDpCp4WG4A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/light-my-request": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/light-my-request/-/light-my-request-6.6.0.tgz", + "integrity": "sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause", + "dependencies": { + "cookie": "^1.0.1", + "process-warning": "^4.0.0", + "set-cookie-parser": "^2.6.0" + } + }, + "node_modules/light-my-request/node_modules/process-warning": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-4.0.1.tgz", + "integrity": "sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, "node_modules/lru-cache": { "version": "10.4.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", @@ -1401,6 +1830,16 @@ "license": "MIT", "peer": true }, + "node_modules/on-exit-leak-free": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/package-json-from-dist": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", @@ -1435,6 +1874,46 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/pino": { + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/pino/-/pino-10.3.1.tgz", + "integrity": "sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pinojs/redact": "^0.4.0", + "atomic-sleep": "^1.0.0", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^3.0.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^5.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^4.0.1", + "thread-stream": "^4.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/pino-abstract-transport": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-3.0.0.tgz", + "integrity": "sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "split2": "^4.0.0" + } + }, + "node_modules/pino-std-serializers": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.1.0.tgz", + "integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==", + "dev": true, + "license": "MIT" + }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", @@ -1445,6 +1924,40 @@ "node": ">= 0.4" } }, + "node_modules/process-warning": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz", + "integrity": "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/quick-format-unescaped": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", + "dev": true, + "license": "MIT" + }, + "node_modules/real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12.13.0" + } + }, "node_modules/reflect-metadata": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", @@ -1461,6 +1974,16 @@ "node": ">=0.10.0" } }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/resolve-pkg-maps": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", @@ -1471,6 +1994,34 @@ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } }, + "node_modules/ret": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.5.0.tgz", + "integrity": "sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "dev": true, + "license": "MIT" + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -1492,6 +2043,76 @@ "license": "MIT", "peer": true }, + "node_modules/safe-regex2": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/safe-regex2/-/safe-regex2-5.1.1.tgz", + "integrity": "sha512-mOSBvHGDZMuIEZMdOz/aCEYDCv0E7nfcNsIhUF+/P+xC7Hyf3FkvymqgPbg9D1EdSGu+uKbJgy09K/RKKc7kJA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "ret": "~0.5.0" + }, + "bin": { + "safe-regex2": "bin/safe-regex2.js" + } + }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/secure-json-parse": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-4.1.0.tgz", + "integrity": "sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "dev": true, + "license": "MIT" + }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -1567,6 +2188,26 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/sonic-boom": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz", + "integrity": "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, "node_modules/sql-highlight": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/sql-highlight/-/sql-highlight-6.1.0.tgz", @@ -1695,6 +2336,26 @@ "node": ">=8" } }, + "node_modules/thread-stream": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-4.2.0.tgz", + "integrity": "sha512-e2zZ96wSChazBsbENf/Pcm/4swHt2cEKQ92rhUjkL9GCKiTDJIaTBenjE/m9DXi0QBmTMDkFDdOomUy20A1tDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "real-require": "^1.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/thread-stream/node_modules/real-require": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-1.0.0.tgz", + "integrity": "sha512-P4nbQYQfePJxRSmY+v/KINxVucm4NF3p3s7pJveMTtom52FR4YGltUQLB8idDXwDDWW+eYrWDFbuzUnjoWHF7g==", + "dev": true, + "license": "MIT" + }, "node_modules/to-buffer": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.2.tgz", @@ -1710,6 +2371,16 @@ "node": ">= 0.4" } }, + "node_modules/toad-cache": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/toad-cache/-/toad-cache-3.7.1.tgz", + "integrity": "sha512-5DXWzE4Vz7xNHsv+xQ+MGfJYyC78Aok3tEr0MNwHoRf7vZnga1mQXZ4/Nsodld4VR6Wd+VhfmqnNrsRJyYPfrQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", diff --git a/nodejs/sqlcommenter-nodejs/packages/sqlcommenter-typeorm/package.json b/nodejs/sqlcommenter-nodejs/packages/sqlcommenter-typeorm/package.json index c07435e5..bd9665a1 100644 --- a/nodejs/sqlcommenter-nodejs/packages/sqlcommenter-typeorm/package.json +++ b/nodejs/sqlcommenter-nodejs/packages/sqlcommenter-typeorm/package.json @@ -1,6 +1,6 @@ { "name": "@query-doctor/sqlcommenter-typeorm", - "version": "0.2.0", + "version": "0.3.0", "description": "SQLCommenter patch for TypeORM", "main": "dist/cjs/index.js", "type": "module", @@ -29,10 +29,21 @@ "types": "./dist/cjs/http.d.ts", "default": "./dist/cjs/http.js" } + }, + "./fastify": { + "import": { + "types": "./dist/esm/fastify.d.ts", + "default": "./dist/esm/fastify.js" + }, + "require": { + "types": "./dist/cjs/fastify.d.ts", + "default": "./dist/cjs/fastify.js" + } } }, "devDependencies": { "@types/node": "^20.19.34", + "fastify": "^5.2.0", "reflect-metadata": "^0.2.2", "sql.js": "^1.12.0", "tsx": "^4.20.5", @@ -43,8 +54,14 @@ }, "peerDependencies": { "@opentelemetry/core": ">=2.8.0", + "fastify": ">=4.0.0", "typeorm": ">=0.3.0" }, + "peerDependenciesMeta": { + "fastify": { + "optional": true + } + }, "engines": { "node": ">=20.0.0" }, diff --git a/nodejs/sqlcommenter-nodejs/packages/sqlcommenter-typeorm/src/fastify.ts b/nodejs/sqlcommenter-nodejs/packages/sqlcommenter-typeorm/src/fastify.ts new file mode 100644 index 00000000..94f66191 --- /dev/null +++ b/nodejs/sqlcommenter-nodejs/packages/sqlcommenter-typeorm/src/fastify.ts @@ -0,0 +1,72 @@ +import type { + FastifyInstance, + FastifyPluginCallback, + FastifyRequest, +} from "fastify"; +import { als } from "./als.js"; +import type { RequestContext } from "./request-context.js"; + +export type SqlcommenterContextFn = ( + request: FastifyRequest, +) => Record; + +export interface SqlcommenterFastifyOptions { + /** + * Extra fields to add to every query's context, merged over the default `route`/`method` + * (e.g. a tenant id, or the matched controller name). Runs once per request in `onRequest`. + */ + context?: SqlcommenterContextFn; +} + +// fastify-plugin sets this symbol so a plugin's hooks/decorators apply to the *parent* scope +// rather than being encapsulated in the plugin's own scope. Our `onRequest` hook has to be +// global so it covers routes — and other plugins' hooks — registered in the parent scope. +// Without it, a plain `register()` silently encapsulates the hook and it tags nothing. This is +// the same primitive `fastify-plugin` relies on, inlined so the integration needs no dependency +// beyond fastify itself. +const SKIP_OVERRIDE = Symbol.for("skip-override"); + +const plugin: FastifyPluginCallback = ( + fastify: FastifyInstance, + options: SqlcommenterFastifyOptions, + done: (err?: Error) => void, +) => { + fastify.addHook("onRequest", (request, _reply, next) => { + const context: RequestContext = { + // `routeOptions.url` is the matched route pattern (e.g. "/items/:id"). It's resolved by the + // time `onRequest` runs; fall back to the raw url for unmatched requests. + route: request.routeOptions?.url ?? request.url, + method: request.method, + ...options.context?.(request), + }; + // Opening the context here — and registering this plugin before any query-issuing plugin — + // means queries from later `onRequest`/`preHandler` hooks and the handler all inherit it, + // not just the handler body. + als.run(context, next); + }); + done(); +}; + +(plugin as unknown as Record)[SKIP_OVERRIDE] = true; + +/** + * Fastify plugin that tags every query issued during a request with its `route` and `method` + * (plus anything returned by `options.context`). + * + * Register it **before** any other plugin whose hooks issue queries (e.g. an auth plugin that + * resolves a session in its own `onRequest`/`preHandler`), so the context is already open when + * those hooks run. Because it hooks `onRequest`, it covers the whole request lifecycle — not + * just the route handler. + * + * @example + * ```ts + * import { sqlcommenterFastify } from "@query-doctor/sqlcommenter-typeorm/fastify"; + * + * await app.register(sqlcommenterFastify); + * await app.register(authPlugin); // queries issued in auth's hooks are tagged too + * ``` + */ +export const sqlcommenterFastify = plugin; +export default sqlcommenterFastify; + +export type { RequestContext } from "./request-context.js"; diff --git a/nodejs/sqlcommenter-nodejs/packages/sqlcommenter-typeorm/src/http.ts b/nodejs/sqlcommenter-nodejs/packages/sqlcommenter-typeorm/src/http.ts index 2efcbb90..5f3a8d9b 100644 --- a/nodejs/sqlcommenter-nodejs/packages/sqlcommenter-typeorm/src/http.ts +++ b/nodejs/sqlcommenter-nodejs/packages/sqlcommenter-typeorm/src/http.ts @@ -2,14 +2,15 @@ import { als } from "./als.js"; import type { RequestContext } from "./request-context.js"; /** - * Wraps the next function in the AsyncLocalStorage with the request context. - * Used to get `route` and `controller` information from the request into the query - * without exposing the underlying AsyncLocalStorage API. + * Runs `next` within an AsyncLocalStorage scope carrying the request context, so any query + * issued during it picks up `route` (and any other provided fields such as `method` and + * `controller`) without exposing the underlying AsyncLocalStorage API. + * + * `next` is invoked for its side effect and its return value is ignored, so the parameter + * accepts any nullary callback — including framework hook callbacks like Fastify's + * `done: (err?: Error) => void` or Express's `next`. */ -export function withRequestContext( - context: RequestContext, - next: () => Promise, -) { +export function withRequestContext(context: RequestContext, next: () => unknown) { als.run(context, next); } diff --git a/nodejs/sqlcommenter-nodejs/packages/sqlcommenter-typeorm/test/fastify.spec.ts b/nodejs/sqlcommenter-nodejs/packages/sqlcommenter-typeorm/test/fastify.spec.ts new file mode 100644 index 00000000..87e80b47 --- /dev/null +++ b/nodejs/sqlcommenter-nodejs/packages/sqlcommenter-typeorm/test/fastify.spec.ts @@ -0,0 +1,56 @@ +import { test } from "node:test"; +import assert from "node:assert"; +import Fastify from "fastify"; +import { sqlcommenterFastify } from "../src/fastify.js"; +import { als } from "../src/als.js"; + +// The plugin is driver-agnostic — it only opens the shared AsyncLocalStorage context that the +// query patch reads. These tests assert that context directly, so they don't need a real ORM. + +// It must apply globally (covering parent-scope routes a plain encapsulated register would miss) +// and cover the whole lifecycle, so an onRequest hook registered after it still sees the context. +test("fastify plugin exposes route/method context lifecycle-wide", async () => { + const seen: Record[] = []; + const app = Fastify(); + await app.register(sqlcommenterFastify); + app.addHook("onRequest", async () => { + const store = als.getStore(); + if (store) seen.push({ phase: "onRequest", ...store }); + }); + app.get("/items/:id", async () => { + const store = als.getStore(); + if (store) seen.push({ phase: "handler", ...store }); + return { ok: true }; + }); + + const res = await app.inject({ method: "GET", url: "/items/42" }); + await app.close(); + + assert.strictEqual(res.statusCode, 200); + assert.deepStrictEqual(seen, [ + { phase: "onRequest", route: "/items/:id", method: "GET" }, + { phase: "handler", route: "/items/:id", method: "GET" }, + ]); +}); + +test("fastify plugin merges extra context fields", async () => { + let store: Record | undefined; + const app = Fastify(); + await app.register(sqlcommenterFastify, { + context: (request) => ({ controller: "items", host: request.headers.host }), + }); + app.get("/items/:id", async () => { + store = als.getStore(); + return { ok: true }; + }); + + await app.inject({ method: "GET", url: "/items/42", headers: { host: "x" } }); + await app.close(); + + assert.deepStrictEqual(store, { + route: "/items/:id", + method: "GET", + controller: "items", + host: "x", + }); +});