Thanks to visit codestin.com
Credit goes to github.com

Skip to content

sql.json recursively mutates object with deepFreeze #227

@nonara

Description

@nonara

Background

I recently ran into a particularly difficult to debug situation.

I have an API Client which uses the http2 library to queue and perform requests to an API server. Attached to this client and each request is a PromiseToken, which acts as a cancellation token class.

After processing GBs of data, I found my code would ultimately fail with Cannot assign to read only property 'waitParent' of object '#'. I searched through all of my compiled code for all interactions with promiseToken and even all calls to Object.freeze, Object.seal, Object.defineProperty, and Object.defineProperties. No interactions or calls that would affect this. I even started digging in to see if it was an issue with Node.

I eventually set up hooks for all of those static methods so that I could figure out what was modifying my PromiseToken.

It turns out what happens is I have an error handler function that is triggered in the event of an HTTP error. This sends the config params to the logger, which adds an entry to the DB. The config params include a reference to my PromiseToken, which means that sql.json(), calls deepFreeze on the token.

Thus, attempting to modify its state later is prevented.

Expected Behavior

The docs describe sql.json() as: Serializes value and binds it as a JSON string literal. The general expectation is that a serializer is a pure function and that it will not mutate the object passed to it.

As an example, imagine if JSON.stringify() mutated the object passed to it.

Possible Solutions

  1. (simple) Add a warning to the documentation that this object will recursively mutate the object passed to it.
    This is a good thing to do to start, but I hope that I can be persuasive in advising against having a serializer mutate the params.

  2. Create a deep copy of value before calling deepFreeze.
    This is a viable solution, however, there is a cost to deep copy that is unnecessary considering that this can be performed synchronously (see solution 3)

  3. Synchronously serialize the information immediately when sql.json is called
    This is bound to be the most performant method and will not require freezing the object, as it is performed synchronously.

Steps to Reproduce

Here's a contrived example showing the problem:

// Get our record info
const rec = await db.one<MyDbRecord>(sql`SELECT * FROM mytable`);

// Add a log
await db.query(`INSERT INTO logs (record) VALUES(${sql.json(rec)})`);

// Update our record
rec.enabled = false; // Error
// ... logic to UPDATE mytable with rec would go here, but it won't get this far

Logs

[DEBUG]:  Ran Spied on Function: Object.freeze()
          PromiseToken {
            cancelled: false,
            children: Set(0) {},
            _resolveWait: undefined,
            onCancel: undefined,
            waitParent: undefined,
            waitPromise: undefined
          }
          at Function.Object.<computed> (/home/ron/node/packages/core/dist/nld/bootstrap.js:83:35)
          at deepFreeze (/home/ron/node/node_modules/slonik/dist/utilities/deepFreeze.js:24:10)
          at deepFreeze (/home/ron/node/node_modules/slonik/dist/utilities/deepFreeze.js:28:7)
          at deepFreeze (/home/ron/node/node_modules/slonik/dist/utilities/deepFreeze.js:28:7)
          at deepFreeze (/home/ron/node/node_modules/slonik/dist/utilities/deepFreeze.js:28:7)
          at deepFreeze (/home/ron/node/node_modules/slonik/dist/utilities/deepFreeze.js:28:7)
          at deepFreeze (/home/ron/node/node_modules/slonik/dist/utilities/deepFreeze.js:28:7)
          at Function.sql.json (/home/ron/node/node_modules/slonik/dist/factories/createSqlTag.js:98:38)
          at /home/ron/node/packages/core/dist/components/logger/logger.js:138:90
          at Logger.logWorker (/home/ron/node/packages/core/dist/components/logger/logger.js:152:15)
          at /home/ron/node/packages/core/dist/components/logger/logger.js:68:64
          at APIClient.errorHandler (/home/ron/node/packages/core/dist/components/api/api-client.js:137:50)
          at runMicrotasks (<anonymous>)
          at processTicksAndRejections (internal/process/task_queues.js:93:5)
          at async AsyncTask.action (/home/ron/node/packages/core/dist/components/api/api-client.js:104:17)

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions