diff --git a/nodejs/sqlcommenter-nodejs/packages/sqlcommenter-knex/index.js b/nodejs/sqlcommenter-nodejs/packages/sqlcommenter-knex/index.js index 7735ae7a..f0f44d44 100644 --- a/nodejs/sqlcommenter-nodejs/packages/sqlcommenter-knex/index.js +++ b/nodejs/sqlcommenter-nodejs/packages/sqlcommenter-knex/index.js @@ -12,113 +12,122 @@ // See the License for the specific language governing permissions and // limitations under the License. -const {hasComment} = require('./util'); -const provider = require('./provider'); -const hook = require('./hooks'); +const { hasComment } = require("./util"); +const provider = require("./provider"); +const hook = require("./hooks"); const defaultFields = { - 'route': true, - 'tracestate': false, - 'traceparent': false, + route: true, + tracestate: false, + traceparent: false, }; /** * All available variables for the commenter are on the `util.fields` object * passing the include parameter will result in each item being excluded from * the commenter output - * + * * @param {Object} Knex * @param {Object} include - A map of values to be optionally included. * @param {Object} options - A configuration object specifying where to collect trace data from. Accepted fields are: * TraceProvider: Should be either 'OpenCensus' or 'OpenTelemetry', indicating which library to collect trace data from. + * @param {Object} additionalComments - A map of strings or functions returning strings that should be included in all comments. * @return {void} */ -exports.wrapMainKnex = (Knex, include={}, options={}) => { - - /* c8 ignore next 2 */ - if (Knex.___alreadySQLCommenterWrapped___) - return; - - const query = Knex.Client.prototype.query; - - // TODO: Contemplate patch for knex's stream prototype - // in addition to the query for commenterization. - - // Please don't change this prototype from an explicit function - // to use arrow functions lest we'll get bugs with not resolving "this". - Knex.Client.prototype.query = function(conn, obj) { - - // If Knex.VERSION() is available, that means they are using a version of knex.js < 0.16.1 - // because as per https://github.com/tgriesser/knex/blob/master/CHANGELOG.md#0161---28-nov-2018 - // Knex.VERSION was removed in favour of `require('knex/package').version` - - const sqlStmt = typeof obj === 'string' ? obj : obj.sql; - - // If a comment already exists, do not insert a new one. - // See internal issue #20. - if (hasComment(sqlStmt)) // Just proceed with the next function ASAP - return query.apply(this, [conn, obj]); - - const knexVersion = getKnexVersion(Knex); - const comments = { - db_driver: `knex:${knexVersion}` - }; - - if (Knex.__middleware__) { - const context = hook.getContext(); - if (context && context.req) { - comments.route = context.req.route.path; - } - } - - // Add trace context to comments, depending on the current provider. - provider.attachComments(options.TraceProvider, comments); - - const filtering = typeof include === 'object' && include && Object.keys(include).length > 0; - // Filter out keys whose values are undefined or aren't to be included by default. - const keys = Object.keys(comments).filter((key) => { - /* c8 ignore next 6 */ - if (!filtering) - return defaultFields[key] && comments[key]; - - // Otherwise since we are filtering, we have to - // see if the field is included and if it set. - return include[key] && comments[key]; - }); - - // Finally sort the keys alphabetically. - keys.sort(); - - const commentStr = keys.map((key) => { - const uri_encoded_key = encodeURIComponent(key); - const uri_encoded_value = encodeURIComponent(comments[key]); - return `${uri_encoded_key}='${uri_encoded_value}'`; - }).join(','); - - if (typeof obj === 'string') { - obj = {sql: `${sqlStmt} /*${commentStr}*/`}; - } else { - obj.sql = `${sqlStmt} /*${commentStr}*/`; - } - - return query.apply(this, [conn, obj]); +exports.wrapMainKnex = ( + Knex, + include = {}, + options = {}, + additionalComments = {} +) => { + /* c8 ignore next 2 */ + if (Knex.___alreadySQLCommenterWrapped___) return; + + const query = Knex.Client.prototype.query; + + // TODO: Contemplate patch for knex's stream prototype + // in addition to the query for commenterization. + + // Please don't change this prototype from an explicit function + // to use arrow functions lest we'll get bugs with not resolving "this". + Knex.Client.prototype.query = function (conn, obj) { + // If Knex.VERSION() is available, that means they are using a version of knex.js < 0.16.1 + // because as per https://github.com/tgriesser/knex/blob/master/CHANGELOG.md#0161---28-nov-2018 + // Knex.VERSION was removed in favour of `require('knex/package').version` + + const sqlStmt = typeof obj === "string" ? obj : obj.sql; + + // If a comment already exists, do not insert a new one. + // See internal issue #20. + if (hasComment(sqlStmt)) + // Just proceed with the next function ASAP + return query.apply(this, [conn, obj]); + + const knexVersion = getKnexVersion(Knex); + const comments = {}; + for (const [key, comment] of Object.entries(additionalComments)) { + comments[key] = typeof comment === "function" ? comment() : comment; } + comments["db_driver"] = `knex:${knexVersion}`; - // Finally mark the object as having already been wrapped. - Knex.___alreadySQLCommenterWrapped___ = true; -} - -const resolveKnexVersion = () => { + if (Knex.__middleware__) { + const context = hook.getContext(); + if (context && context.req) { + comments.route = context.req.route.path; + } + } - try { - return require('knex/package').version; - } catch (err) { - // Perhaps they are using an old version of knex.js. - // That is because knex.js as per - // https://github.com/tgriesser/knex/blob/master/CHANGELOG.md#0161---28-nov-2018 - // Knex.VERSION() was removed in favor of `require('knex/package').version` - return null; + // Add trace context to comments, depending on the current provider. + provider.attachComments(options.TraceProvider, comments); + + const filtering = + typeof include === "object" && include && Object.keys(include).length > 0; + // Filter out keys whose values are undefined or aren't to be included by default. + const keys = Object.keys(comments).filter((key) => { + if (additionalComments[key]) return true; + + /* c8 ignore next 6 */ + if (!filtering) return defaultFields[key] && comments[key]; + + // Otherwise since we are filtering, we have to + // see if the field is included and if it set. + return include[key] && comments[key]; + }); + + // Finally sort the keys alphabetically. + keys.sort(); + + const commentStr = keys + .map((key) => { + const uri_encoded_key = encodeURIComponent(key); + const uri_encoded_value = encodeURIComponent(comments[key]); + return `${uri_encoded_key}='${uri_encoded_value}'`; + }) + .join(","); + + if (typeof obj === "string") { + obj = { sql: `${sqlStmt} /*${commentStr}*/` }; + } else { + obj.sql = `${sqlStmt} /*${commentStr}*/`; } + + return query.apply(this, [conn, obj]); + }; + + // Finally mark the object as having already been wrapped. + Knex.___alreadySQLCommenterWrapped___ = true; +}; + +const resolveKnexVersion = () => { + try { + return require("knex/package").version; + } catch (err) { + // Perhaps they are using an old version of knex.js. + // That is because knex.js as per + // https://github.com/tgriesser/knex/blob/master/CHANGELOG.md#0161---28-nov-2018 + // Knex.VERSION() was removed in favor of `require('knex/package').version` + return null; + } }; // Since resolveKnexVersion performs expensive lookups by imports, @@ -127,28 +136,29 @@ const resolvedKnexVersion = resolveKnexVersion(); // Use getKnexVersion to find out the version of knex being used. const getKnexVersion = (Knex) => { - return Knex && typeof Knex.VERSION === 'function' ? Knex.VERSION() : resolvedKnexVersion; -} + return Knex && typeof Knex.VERSION === "function" + ? Knex.VERSION() + : resolvedKnexVersion; +}; /** * All available variables for the commenter are on the `util.fields` object * passing the include parameter will result items not available in that map * only being included in the comment. - * - * @param {Object} Knex + * + * @param {Object} Knex * @param {Object} include - A map of variables to include. If unset, we'll use default attributes. * @param {Object} options - A configuration object specifying where to collect trace data from. Accepted fields are: * TraceProvider: Should be either 'OpenCensus' or 'OpenTelemetry', indicating which library to collect trace data from. * @return {Function} A middleware that is compatible with the express framework. */ -exports.wrapMainKnexAsMiddleware = (Knex, include=null, options) => { - - exports.wrapMainKnex(Knex, include, options); - - return (req, res, next) => { - data = { req: req }; - hook.createContext(data); - Knex.__middleware__ = true; - next(); - } -} +exports.wrapMainKnexAsMiddleware = (Knex, include = null, options) => { + exports.wrapMainKnex(Knex, include, options); + + return (req, res, next) => { + data = { req: req }; + hook.createContext(data); + Knex.__middleware__ = true; + next(); + }; +}; diff --git a/nodejs/sqlcommenter-nodejs/packages/sqlcommenter-knex/test/comment.test.js b/nodejs/sqlcommenter-nodejs/packages/sqlcommenter-knex/test/comment.test.js index a3ac3aa4..0045fc94 100644 --- a/nodejs/sqlcommenter-nodejs/packages/sqlcommenter-knex/test/comment.test.js +++ b/nodejs/sqlcommenter-nodejs/packages/sqlcommenter-knex/test/comment.test.js @@ -14,155 +14,200 @@ "use strict"; -const { wrapMainKnex } = require('../index'); +const { wrapMainKnex } = require("../index"); const chai = require("chai"); -const { context, trace } = require('@opentelemetry/api'); -const { NodeTracerProvider } = require('@opentelemetry/node'); -const { AsyncHooksContextManager } = require('@opentelemetry/context-async-hooks'); -const { InMemorySpanExporter, SimpleSpanProcessor } = require('@opentelemetry/tracing'); +const { context, trace } = require("@opentelemetry/api"); +const { NodeTracerProvider } = require("@opentelemetry/node"); +const { + AsyncHooksContextManager, +} = require("@opentelemetry/context-async-hooks"); +const { + InMemorySpanExporter, + SimpleSpanProcessor, +} = require("@opentelemetry/tracing"); const expect = chai.expect; describe("Comments for Knex", () => { - - let fakeKnex = { - Client: { - prototype: { - config: { connection: { database: 'fake' }, client: 'fakesql' }, - version: 'fake-server:0.0.X', - query: (conn, obj) => { - return Promise.resolve(obj); // simply returns a resolved promise for inspection. - } - } + let fakeKnex = { + Client: { + prototype: { + config: { connection: { database: "fake" }, client: "fakesql" }, + version: "fake-server:0.0.X", + query: (conn, obj) => { + return Promise.resolve(obj); // simply returns a resolved promise for inspection. }, - VERSION: () => { - return 'fake:0.0.1'; - } - }; - - before(() => { - wrapMainKnex(fakeKnex, { db_driver: true }) + }, + }, + VERSION: () => { + return "fake:0.0.1"; + }, + }; + + before(() => { + wrapMainKnex(fakeKnex, { db_driver: true }); + }); + + describe("Cases", () => { + it("should add comment to generated sql", (done) => { + const want = + "SELECT CURRENT_TIMESTAMP /*db_driver='knex%3Afake%3A0.0.1'*/"; + const obj = { sql: "SELECT CURRENT_TIMESTAMP" }; + + fakeKnex.Client.prototype.query(null, obj).then(({ sql }) => { + expect(sql).equals(want); + }); + done(); }); - describe("Cases", () => { - - it("should add comment to generated sql", (done) => { - - const want = "SELECT CURRENT_TIMESTAMP /*db_driver='knex%3Afake%3A0.0.1'*/"; - const obj = { sql: 'SELECT CURRENT_TIMESTAMP' }; - - fakeKnex.Client.prototype.query(null, obj).then(({ sql }) => { - expect(sql).equals(want); - }); - done(); - }); - - it("should NOT affix comments to statements with existing comments", (done) => { - - const queries = [ - 'SELECT * FROM people /* existing */', - 'SELECT * FROM people -- existing' - ]; - - Promise.all([ - fakeKnex.Client.prototype.query(null, queries[0]), - fakeKnex.Client.prototype.query(null, queries[1]) - ]).then(([a, b]) => { - expect(a).to.equal(queries[0]); - expect(b).to.equal(queries[1]); - }); - done(); - }); + it("should NOT affix comments to statements with existing comments", (done) => { + const queries = [ + "SELECT * FROM people /* existing */", + "SELECT * FROM people -- existing", + ]; + + Promise.all([ + fakeKnex.Client.prototype.query(null, queries[0]), + fakeKnex.Client.prototype.query(null, queries[1]), + ]).then(([a, b]) => { + expect(a).to.equal(queries[0]); + expect(b).to.equal(queries[1]); + }); + done(); + }); - it("should add expected database/driver properties", (done) => { - const want = [ - "db_driver", - ]; - fakeKnex.Client.prototype.query(null, 'SELECT * from foo').then(({ sql }) => { - want.forEach((key) => { - expect(sql.indexOf(key)).to.be.gt(-1); - }); - }); - done(); + it("should add expected database/driver properties", (done) => { + const want = ["db_driver"]; + fakeKnex.Client.prototype + .query(null, "SELECT * from foo") + .then(({ sql }) => { + want.forEach((key) => { + expect(sql.indexOf(key)).to.be.gt(-1); + }); }); + done(); + }); - it("should deterministically sort keys alphabetically", (done) => { - const want = "SELECT * from foo /*db_driver='knex%3Afake%3A0.0.1'*/"; - fakeKnex.Client.prototype.query(null, { sql: 'SELECT * from foo' }).then(({ sql }) => { - expect(sql).equals(want); - }); - done(); + it("should deterministically sort keys alphabetically", (done) => { + const want = "SELECT * from foo /*db_driver='knex%3Afake%3A0.0.1'*/"; + fakeKnex.Client.prototype + .query(null, { sql: "SELECT * from foo" }) + .then(({ sql }) => { + expect(sql).equals(want); }); + done(); + }); - it("chaining and repeated calls should NOT indefinitely chain SQL", (done) => { - - const want = "SELECT * from foo /*db_driver='knex%3Afake%3A0.0.1'*/"; - - const obj = { sql: 'SELECT * from foo' }; + it("chaining and repeated calls should NOT indefinitely chain SQL", (done) => { + const want = "SELECT * from foo /*db_driver='knex%3Afake%3A0.0.1'*/"; - fakeKnex.Client.prototype.query(null, obj) - .then((a) => fakeKnex.Client.prototype.query(null, a)) - .then((b) => fakeKnex.Client.prototype.query(null, b)) - .then((c) => fakeKnex.Client.prototype.query(null, c)) - .then((d) => { - expect(d.sql).equals(want); - }); + const obj = { sql: "SELECT * from foo" }; - done(); + fakeKnex.Client.prototype + .query(null, obj) + .then((a) => fakeKnex.Client.prototype.query(null, a)) + .then((b) => fakeKnex.Client.prototype.query(null, b)) + .then((c) => fakeKnex.Client.prototype.query(null, c)) + .then((d) => { + expect(d.sql).equals(want); }); + + done(); }); + }); }); - describe("With OpenTelemetry tracing", () => { - - let fakeKnex = { - Client: { - prototype: { - config: { connection: { database: 'fake' }, client: 'fakesql' }, - version: 'fake-server:0.0.X', - query: (conn, obj) => { - return Promise.resolve(obj); // simply returns a resolved promise for inspection. - } - } + let fakeKnex = { + Client: { + prototype: { + config: { connection: { database: "fake" }, client: "fakesql" }, + version: "fake-server:0.0.X", + query: (conn, obj) => { + return Promise.resolve(obj); // simply returns a resolved promise for inspection. }, - VERSION: () => { - return 'fake:0.0.1'; - } - }; - - // Load OpenTelemetry components - const provider = new NodeTracerProvider(); - const memoryExporter = new InMemorySpanExporter(); - const spanProcessor = new SimpleSpanProcessor(memoryExporter); - provider.addSpanProcessor(spanProcessor); - const tracer = provider.getTracer('default'); - trace.setGlobalTracerProvider(provider); - let contextManager; - - before(() => { - contextManager = new AsyncHooksContextManager(); - context.setGlobalContextManager(contextManager.enable()); - wrapMainKnex(fakeKnex, { db_driver: true, traceparent: true, tracestate: true }, { TraceProvider: "" }); + }, + }, + VERSION: () => { + return "fake:0.0.1"; + }, + }; + + // Load OpenTelemetry components + const provider = new NodeTracerProvider(); + const memoryExporter = new InMemorySpanExporter(); + const spanProcessor = new SimpleSpanProcessor(memoryExporter); + provider.addSpanProcessor(spanProcessor); + const tracer = provider.getTracer("default"); + trace.setGlobalTracerProvider(provider); + let contextManager; + + before(() => { + contextManager = new AsyncHooksContextManager(); + context.setGlobalContextManager(contextManager.enable()); + wrapMainKnex( + fakeKnex, + { db_driver: true, traceparent: true, tracestate: true }, + { TraceProvider: "" } + ); + }); + + after(() => { + memoryExporter.reset(); + context.disable(); + }); + + it("Starting an OpenTelemetry trace should produce `traceparent`", (done) => { + const rootSpan = tracer.startSpan("rootSpan"); + context.with(trace.setSpan(context.active(), rootSpan), async () => { + const obj = { sql: "SELECT * FROM foo" }; + fakeKnex.Client.prototype.query(null, obj).then((got) => { + const augmentedSQL = got.sql; + const wantSQL = `SELECT * FROM foo /*db_driver='knex%3Afake%3A0.0.1',traceparent='00-${ + rootSpan.spanContext().traceId + }-${rootSpan.spanContext().spanId}-01'*/`; + console.log(augmentedSQL); + expect(augmentedSQL).equals(wantSQL); + rootSpan.end(); + done(); + }); }); + }); +}); - after(() => { - memoryExporter.reset(); - context.disable(); +describe("Additional comments for Knex", () => { + let fakeKnex = { + Client: { + prototype: { + config: { connection: { database: "fake" }, client: "fakesql" }, + version: "fake-server:0.0.X", + query: (conn, obj) => { + return Promise.resolve(obj); // simply returns a resolved promise for inspection. + }, + }, + }, + VERSION: () => { + return "fake:0.0.1"; + }, + }; + + before(() => { + wrapMainKnex(fakeKnex, { db_driver: true }, undefined, { + key1: "someString", + key2: () => "stringFromFn", }); - - it('Starting an OpenTelemetry trace should produce `traceparent`', (done) => { - const rootSpan = tracer.startSpan('rootSpan'); - context.with(trace.setSpan(context.active(), rootSpan), async () => { - const obj = { sql: 'SELECT * FROM foo' }; - fakeKnex.Client.prototype.query(null, obj).then((got) => { - const augmentedSQL = got.sql; - const wantSQL = `SELECT * FROM foo /*db_driver='knex%3Afake%3A0.0.1',traceparent='00-${rootSpan.spanContext().traceId}-${rootSpan.spanContext().spanId}-01'*/`; - console.log(augmentedSQL); - expect(augmentedSQL).equals(wantSQL); - rootSpan.end(); - done(); - }); - }); + }); + + describe("Cases", () => { + it("should add comment to generated sql", (done) => { + const want = + "SELECT CURRENT_TIMESTAMP /*db_driver='knex%3Afake%3A0.0.1',key1='someString',key2='stringFromFn'*/"; + const obj = { sql: "SELECT CURRENT_TIMESTAMP" }; + + fakeKnex.Client.prototype.query(null, obj).then(({ sql }) => { + expect(sql).equals(want); + }); + done(); }); + }); });