kysely icon indicating copy to clipboard operation
kysely copied to clipboard

Passing context to plugins on a query by query basis

Open jordanjoyce opened this issue 2 years ago • 8 comments

Have a use case where I need to stop certain columns from transforming to snake case. I've forked the CamelCasePlugin and got the functionality working however to get this to work on a query by query basis I currently need to use the following pattern

qb.withoutPlugins().withPlugin(new CamelCasePlugin({}, noTransformCols]))

I've recently migrated from Knex and they had a queryContext function that I used for similar functionality. Wondering if there's an easier way to do this that I've missed or not.

Thanks

jordanjoyce avatar Aug 02 '23 06:08 jordanjoyce

Hey 👋

To get this to work per-query, all you have to do is use .withPlugin(new CamelCasePlugin({}, noTransformCols)). I don't understand, from your post, why you'd need .withoutPlugins().

igalklebanov avatar Aug 05 '23 18:08 igalklebanov

@igalklebanov Thanks for the quick response, should have mentioned the CamelCasePlugin is used globally, so I have to use .withoutPlugins() to stop it running twice. This pattern is fine as it's only used in a few queries. Was just reaching out to make sure I wasn't missing a more obvious solution!

jordanjoyce avatar Aug 06 '23 22:08 jordanjoyce

@jordanjoyce I see. Given your usage, there is no better way right now.

@koskimas a few ideas...

maybe we could offer some kind of state that can be global and local (overriding globals) that all plugins and drivers can consume? this would remove the forced usage of .withoutPlugins (that can be risky - removing more than you need) or .withPlugin (no need to instantiate a new plugin instance per invocation) everywhere.

const db = new Kysely<DB>({
  // ...
  plugins: [new MyCamelCasePlugin()],
  vars: {
    // some global vars.
  }
})

db
  .$withVars({ noCamelCase: ['column_0', 'column_1'] })
  .selectFrom('table').select(['column_0', 'column_1', 'column_2'])
  .execute()

This can also facilitate prepared statements (passing names as vars and driver consuming these to prepare if not prepared with that name yet).

plugin dedup/override by some name.

igalklebanov avatar Aug 07 '23 13:08 igalklebanov

Don't know about this. I added something like this to objection and it opened a door for all kinds of hacks people shouldn't have done. I'm afraid adding something like this will make people add ORM features as plugins which just leads to a really really shitty ORM and a LOT of issues and feature requests for us to solve.

This is not optimal but you could do something like this

export db = new Kysely<DB>({
  dialect,
  plugins: PLUGINS
})
export const PLUGINS = [
  new CamelCasePlugin(camelCaseConfig),
  ...OTHER_PLUGINS
]

export function noCamelCaseFor(columns: string[]) {
  return <T extends SelectQueryBuilder<any, any, any>>(qb: T) {
   let qb = qb
     .withoutPlugins()
     .withPlugin(new CamelCasePlugin(camelCaseConfig), columns)
    
    for (const p of OTHER_PLUGINS) {
      qb = qb.withPlugin(p)
    }

    return qb
  }
}

And usage:

db.selectFrom('person')
  .select('first_name')
  .$call(noCamelCaseFor(['first_name']))

koskimas avatar Sep 08 '23 05:09 koskimas

Don't know about this. I added something like this to objection and it opened a door for all kinds of hacks people shouldn't have done. I'm afraid adding something like this will make people add ORM features as plugins which just leads to a really really shitty ORM and a LOT of issues and feature requests for us to solve.

This is not optimal but you could do something like this

export db = new Kysely<DB>({
  dialect,
  plugins: PLUGINS
})
export const PLUGINS = [
  new CamelCasePlugin(camelCaseConfig),
  ...OTHER_PLUGINS
]

export function noCamelCaseFor(columns: string[]) {
  return <T extends SelectQueryBuilder<any, any, any>>(qb: T) {
   let qb = qb
     .withoutPlugins()
     .withPlugin(new CamelCasePlugin(camelCaseConfig), columns)
    
    for (const p of OTHER_PLUGINS) {
      qb = qb.withPlugin(p)
    }

    return qb
  }
}

And usage:

db.selectFrom('person')
  .select('first_name')
  .$call(noCamelCaseFor(['first_name']))

qb does not have withoutPlugins method. Can you suggest any other way?

geetesh911 avatar Jan 29 '24 15:01 geetesh911

I also have a plugin that I’d like to be able to opt-out of for individual queries. If something like $withVars is a bad idea, maybe two new query builder methods like this could be convenient?

// A new overload for withoutPlugins that returns an 
// identical query builder with just these plugins removed
// (where each plugin is identified by === equality)
withoutPlugins(plugins: readonly KyselyPlugin[]): …

// returns a qb that retains only those plugins which pass the predicate.
// Useful when the consumer doesn’t have a reference to
// the exact plugin object (including in certain mocking/testing scenarios)
filterPlugins(predicate: (it: KyselyPlugin) => boolean): …

I’m not sure if those methods are worth it, but their intention seems slightly clearer, and they could be implemented more efficiently than the version shown above where the plugins have to be added back one-by-one, creating a new qb each time.

ethanresnick avatar Apr 05 '24 20:04 ethanresnick