Initial commit - Event Planner application

This commit is contained in:
mberlin
2026-03-18 14:55:56 -03:00
commit 86d779eb4d
7548 changed files with 1006324 additions and 0 deletions

View File

@@ -0,0 +1,170 @@
import type { QueryResult } from '../../driver/database-connection.js';
import type { RootOperationNode } from '../../query-compiler/query-compiler.js';
import type { UnknownRow } from '../../util/type-utils.js';
import type { KyselyPlugin, PluginTransformQueryArgs, PluginTransformResultArgs } from '../kysely-plugin.js';
export interface CamelCasePluginOptions {
/**
* If true, camelCase is transformed into upper case SNAKE_CASE.
* For example `fooBar => FOO_BAR` and `FOO_BAR => fooBar`
*
* Defaults to false.
*/
upperCase?: boolean;
/**
* If true, an underscore is added before each digit when converting
* camelCase to snake_case. For example `foo12Bar => foo_12_bar` and
* `foo_12_bar => foo12Bar`
*
* Defaults to false.
*/
underscoreBeforeDigits?: boolean;
/**
* If true, an underscore is added between consecutive upper case
* letters when converting from camelCase to snake_case. For example
* `fooBAR => foo_b_a_r` and `foo_b_a_r => fooBAR`.
*
* Defaults to false.
*/
underscoreBetweenUppercaseLetters?: boolean;
/**
* If true, nested object's keys will not be converted to camel case.
*
* Defaults to false.
*/
maintainNestedObjectKeys?: boolean;
}
/**
* A plugin that converts snake_case identifiers in the database into
* camelCase in the JavaScript side.
*
* For example let's assume we have a table called `person_table`
* with columns `first_name` and `last_name` in the database. When
* using `CamelCasePlugin` we would setup Kysely like this:
*
* ```ts
* import * as Sqlite from 'better-sqlite3'
* import { CamelCasePlugin, Kysely, SqliteDialect } from 'kysely'
*
* interface CamelCasedDatabase {
* userMetadata: {
* firstName: string
* lastName: string
* }
* }
*
* const db = new Kysely<CamelCasedDatabase>({
* dialect: new SqliteDialect({
* database: new Sqlite(':memory:'),
* }),
* plugins: [new CamelCasePlugin()],
* })
*
* const person = await db.selectFrom('userMetadata')
* .where('firstName', '=', 'Arnold')
* .select(['firstName', 'lastName'])
* .executeTakeFirst()
*
* if (person) {
* console.log(person.firstName)
* }
* ```
*
* The generated SQL (SQLite):
*
* ```sql
* select "first_name", "last_name" from "user_metadata" where "first_name" = ?
* ```
*
* As you can see from the example, __everything__ needs to be defined
* in camelCase in the TypeScript code: table names, columns, schemas,
* __everything__. When using the `CamelCasePlugin` Kysely works as if
* the database was defined in camelCase.
*
* There are various options you can give to the plugin to modify
* the way identifiers are converted. See {@link CamelCasePluginOptions}.
* If those options are not enough, you can override this plugin's
* `snakeCase` and `camelCase` methods to make the conversion exactly
* the way you like:
*
* ```ts
* class MyCamelCasePlugin extends CamelCasePlugin {
* protected override snakeCase(str: string): string {
* // ...
*
* return str
* }
*
* protected override camelCase(str: string): string {
* // ...
*
* return str
* }
* }
* ```
*/
export declare class CamelCasePlugin implements KyselyPlugin {
#private;
readonly opt: CamelCasePluginOptions;
constructor(opt?: CamelCasePluginOptions);
/**
* This is called for each query before it is executed. You can modify the query by
* transforming its {@link OperationNode} tree provided in {@link PluginTransformQueryArgs.node | args.node}
* and returning the transformed tree. You'd usually want to use an {@link OperationNodeTransformer}
* for this.
*
* If you need to pass some query-related data between this method and `transformResult` you
* can use a `WeakMap` with {@link PluginTransformQueryArgs.queryId | args.queryId} as the key:
*
* ```ts
* import type {
* KyselyPlugin,
* QueryResult,
* RootOperationNode,
* UnknownRow
* } from 'kysely'
*
* interface MyData {
* // ...
* }
* const data = new WeakMap<any, MyData>()
*
* const plugin = {
* transformQuery(args: PluginTransformQueryArgs): RootOperationNode {
* const something: MyData = {}
*
* // ...
*
* data.set(args.queryId, something)
*
* // ...
*
* return args.node
* },
*
* async transformResult(args: PluginTransformResultArgs): Promise<QueryResult<UnknownRow>> {
* // ...
*
* const something = data.get(args.queryId)
*
* // ...
*
* return args.result
* }
* } satisfies KyselyPlugin
* ```
*
* You should use a `WeakMap` instead of a `Map` or some other strong references because `transformQuery`
* is not always matched by a call to `transformResult` which would leave orphaned items in the map
* and cause a memory leak.
*/
transformQuery(args: PluginTransformQueryArgs): RootOperationNode;
/**
* This method is called for each query after it has been executed. The result
* of the query can be accessed through {@link PluginTransformResultArgs.result | args.result}.
* You can modify the result and return the modifier result.
*/
transformResult(args: PluginTransformResultArgs): Promise<QueryResult<UnknownRow>>;
protected mapRow(row: UnknownRow): UnknownRow;
protected snakeCase(str: string): string;
protected camelCase(str: string): string;
}

View File

@@ -0,0 +1,119 @@
/// <reference types="./camel-case-plugin.d.ts" />
import { isPlainObject } from '../../util/object-utils.js';
import { SnakeCaseTransformer } from './camel-case-transformer.js';
import { createCamelCaseMapper, createSnakeCaseMapper, } from './camel-case.js';
/**
* A plugin that converts snake_case identifiers in the database into
* camelCase in the JavaScript side.
*
* For example let's assume we have a table called `person_table`
* with columns `first_name` and `last_name` in the database. When
* using `CamelCasePlugin` we would setup Kysely like this:
*
* ```ts
* import * as Sqlite from 'better-sqlite3'
* import { CamelCasePlugin, Kysely, SqliteDialect } from 'kysely'
*
* interface CamelCasedDatabase {
* userMetadata: {
* firstName: string
* lastName: string
* }
* }
*
* const db = new Kysely<CamelCasedDatabase>({
* dialect: new SqliteDialect({
* database: new Sqlite(':memory:'),
* }),
* plugins: [new CamelCasePlugin()],
* })
*
* const person = await db.selectFrom('userMetadata')
* .where('firstName', '=', 'Arnold')
* .select(['firstName', 'lastName'])
* .executeTakeFirst()
*
* if (person) {
* console.log(person.firstName)
* }
* ```
*
* The generated SQL (SQLite):
*
* ```sql
* select "first_name", "last_name" from "user_metadata" where "first_name" = ?
* ```
*
* As you can see from the example, __everything__ needs to be defined
* in camelCase in the TypeScript code: table names, columns, schemas,
* __everything__. When using the `CamelCasePlugin` Kysely works as if
* the database was defined in camelCase.
*
* There are various options you can give to the plugin to modify
* the way identifiers are converted. See {@link CamelCasePluginOptions}.
* If those options are not enough, you can override this plugin's
* `snakeCase` and `camelCase` methods to make the conversion exactly
* the way you like:
*
* ```ts
* class MyCamelCasePlugin extends CamelCasePlugin {
* protected override snakeCase(str: string): string {
* // ...
*
* return str
* }
*
* protected override camelCase(str: string): string {
* // ...
*
* return str
* }
* }
* ```
*/
export class CamelCasePlugin {
opt;
#camelCase;
#snakeCase;
#snakeCaseTransformer;
constructor(opt = {}) {
this.opt = opt;
this.#camelCase = createCamelCaseMapper(opt);
this.#snakeCase = createSnakeCaseMapper(opt);
this.#snakeCaseTransformer = new SnakeCaseTransformer(this.snakeCase.bind(this));
}
transformQuery(args) {
return this.#snakeCaseTransformer.transformNode(args.node, args.queryId);
}
async transformResult(args) {
if (args.result.rows && Array.isArray(args.result.rows)) {
return {
...args.result,
rows: args.result.rows.map((row) => this.mapRow(row)),
};
}
return args.result;
}
mapRow(row) {
return Object.keys(row).reduce((obj, key) => {
let value = row[key];
if (Array.isArray(value)) {
value = value.map((it) => (canMap(it, this.opt) ? this.mapRow(it) : it));
}
else if (canMap(value, this.opt)) {
value = this.mapRow(value);
}
obj[this.camelCase(key)] = value;
return obj;
}, {});
}
snakeCase(str) {
return this.#snakeCase(str);
}
camelCase(str) {
return this.#camelCase(str);
}
}
function canMap(obj, opt) {
return isPlainObject(obj) && !opt?.maintainNestedObjectKeys;
}

View File

@@ -0,0 +1,9 @@
import type { IdentifierNode } from '../../operation-node/identifier-node.js';
import { OperationNodeTransformer } from '../../operation-node/operation-node-transformer.js';
import type { QueryId } from '../../util/query-id.js';
import type { StringMapper } from './camel-case.js';
export declare class SnakeCaseTransformer extends OperationNodeTransformer {
#private;
constructor(snakeCase: StringMapper);
protected transformIdentifier(node: IdentifierNode, queryId: QueryId): IdentifierNode;
}

View File

@@ -0,0 +1,16 @@
/// <reference types="./camel-case-transformer.d.ts" />
import { OperationNodeTransformer } from '../../operation-node/operation-node-transformer.js';
export class SnakeCaseTransformer extends OperationNodeTransformer {
#snakeCase;
constructor(snakeCase) {
super();
this.#snakeCase = snakeCase;
}
transformIdentifier(node, queryId) {
node = super.transformIdentifier(node, queryId);
return {
...node,
name: this.#snakeCase(node.name),
};
}
}

View File

@@ -0,0 +1,15 @@
export type StringMapper = (str: string) => string;
/**
* Creates a function that transforms camel case strings to snake case.
*/
export declare function createSnakeCaseMapper({ upperCase, underscoreBeforeDigits, underscoreBetweenUppercaseLetters, }?: {
upperCase?: boolean | undefined;
underscoreBeforeDigits?: boolean | undefined;
underscoreBetweenUppercaseLetters?: boolean | undefined;
}): StringMapper;
/**
* Creates a function that transforms snake case strings to camel case.
*/
export declare function createCamelCaseMapper({ upperCase, }?: {
upperCase?: boolean | undefined;
}): StringMapper;

View File

@@ -0,0 +1,108 @@
/// <reference types="./camel-case.d.ts" />
/**
* Creates a function that transforms camel case strings to snake case.
*/
export function createSnakeCaseMapper({ upperCase = false, underscoreBeforeDigits = false, underscoreBetweenUppercaseLetters = false, } = {}) {
return memoize((str) => {
if (str.length === 0) {
return str;
}
const upper = str.toUpperCase();
const lower = str.toLowerCase();
let out = lower[0];
for (let i = 1, l = str.length; i < l; ++i) {
const char = str[i];
const prevChar = str[i - 1];
const upperChar = upper[i];
const prevUpperChar = upper[i - 1];
const lowerChar = lower[i];
const prevLowerChar = lower[i - 1];
// If underScoreBeforeDigits is true then, well, insert an underscore
// before digits :). Only the first digit gets an underscore if
// there are multiple.
if (underscoreBeforeDigits &&
isDigit(char) &&
!isDigit(prevChar) &&
!out.endsWith('_')) {
out += '_' + char;
continue;
}
// Test if `char` is an upper-case character and that the character
// actually has different upper and lower case versions.
if (char === upperChar && upperChar !== lowerChar) {
const prevCharacterIsUppercase = prevChar === prevUpperChar && prevUpperChar !== prevLowerChar;
// If underscoreBetweenUppercaseLetters is true, we always place an underscore
// before consecutive uppercase letters (e.g. "fooBAR" becomes "foo_b_a_r").
// Otherwise, we don't (e.g. "fooBAR" becomes "foo_bar").
if (underscoreBetweenUppercaseLetters || !prevCharacterIsUppercase) {
out += '_' + lowerChar;
}
else {
out += lowerChar;
}
}
else {
out += char;
}
}
if (upperCase) {
return out.toUpperCase();
}
else {
return out;
}
});
}
/**
* Creates a function that transforms snake case strings to camel case.
*/
export function createCamelCaseMapper({ upperCase = false, } = {}) {
return memoize((str) => {
if (str.length === 0) {
return str;
}
if (upperCase && isAllUpperCaseSnakeCase(str)) {
// Only convert to lower case if the string is all upper
// case snake_case. This allows camelCase strings to go
// through without changing.
str = str.toLowerCase();
}
let out = str[0];
for (let i = 1, l = str.length; i < l; ++i) {
const char = str[i];
const prevChar = str[i - 1];
if (char !== '_') {
if (prevChar === '_') {
out += char.toUpperCase();
}
else {
out += char;
}
}
}
return out;
});
}
function isAllUpperCaseSnakeCase(str) {
for (let i = 1, l = str.length; i < l; ++i) {
const char = str[i];
if (char !== '_' && char !== char.toUpperCase()) {
return false;
}
}
return true;
}
function isDigit(char) {
return char >= '0' && char <= '9';
}
function memoize(func) {
const cache = new Map();
return (str) => {
let mapped = cache.get(str);
if (!mapped) {
mapped = func(str);
cache.set(str, mapped);
}
return mapped;
};
}

View File

@@ -0,0 +1,70 @@
import type { QueryResult } from '../../driver/database-connection.js';
import type { RootOperationNode } from '../../query-compiler/query-compiler.js';
import type { UnknownRow } from '../../util/type-utils.js';
import type { KyselyPlugin, PluginTransformQueryArgs, PluginTransformResultArgs } from '../kysely-plugin.js';
/**
* Plugin that removes duplicate joins from queries.
*
* See [this recipe](https://github.com/kysely-org/kysely/blob/master/site/docs/recipes/0008-deduplicate-joins.md)
*/
export declare class DeduplicateJoinsPlugin implements KyselyPlugin {
#private;
/**
* This is called for each query before it is executed. You can modify the query by
* transforming its {@link OperationNode} tree provided in {@link PluginTransformQueryArgs.node | args.node}
* and returning the transformed tree. You'd usually want to use an {@link OperationNodeTransformer}
* for this.
*
* If you need to pass some query-related data between this method and `transformResult` you
* can use a `WeakMap` with {@link PluginTransformQueryArgs.queryId | args.queryId} as the key:
*
* ```ts
* import type {
* KyselyPlugin,
* QueryResult,
* RootOperationNode,
* UnknownRow
* } from 'kysely'
*
* interface MyData {
* // ...
* }
* const data = new WeakMap<any, MyData>()
*
* const plugin = {
* transformQuery(args: PluginTransformQueryArgs): RootOperationNode {
* const something: MyData = {}
*
* // ...
*
* data.set(args.queryId, something)
*
* // ...
*
* return args.node
* },
*
* async transformResult(args: PluginTransformResultArgs): Promise<QueryResult<UnknownRow>> {
* // ...
*
* const something = data.get(args.queryId)
*
* // ...
*
* return args.result
* }
* } satisfies KyselyPlugin
* ```
*
* You should use a `WeakMap` instead of a `Map` or some other strong references because `transformQuery`
* is not always matched by a call to `transformResult` which would leave orphaned items in the map
* and cause a memory leak.
*/
transformQuery(args: PluginTransformQueryArgs): RootOperationNode;
/**
* This method is called for each query after it has been executed. The result
* of the query can be accessed through {@link PluginTransformResultArgs.result | args.result}.
* You can modify the result and return the modifier result.
*/
transformResult(args: PluginTransformResultArgs): Promise<QueryResult<UnknownRow>>;
}

View File

@@ -0,0 +1,16 @@
/// <reference types="./deduplicate-joins-plugin.d.ts" />
import { DeduplicateJoinsTransformer } from './deduplicate-joins-transformer.js';
/**
* Plugin that removes duplicate joins from queries.
*
* See [this recipe](https://github.com/kysely-org/kysely/blob/master/site/docs/recipes/0008-deduplicate-joins.md)
*/
export class DeduplicateJoinsPlugin {
#transformer = new DeduplicateJoinsTransformer();
transformQuery(args) {
return this.#transformer.transformNode(args.node, args.queryId);
}
transformResult(args) {
return Promise.resolve(args.result);
}
}

View File

@@ -0,0 +1,11 @@
import type { DeleteQueryNode } from '../../operation-node/delete-query-node.js';
import { OperationNodeTransformer } from '../../operation-node/operation-node-transformer.js';
import type { SelectQueryNode } from '../../operation-node/select-query-node.js';
import type { UpdateQueryNode } from '../../operation-node/update-query-node.js';
import type { QueryId } from '../../util/query-id.js';
export declare class DeduplicateJoinsTransformer extends OperationNodeTransformer {
#private;
protected transformSelectQuery(node: SelectQueryNode, queryId: QueryId): SelectQueryNode;
protected transformUpdateQuery(node: UpdateQueryNode, queryId: QueryId): UpdateQueryNode;
protected transformDeleteQuery(node: DeleteQueryNode, queryId: QueryId): DeleteQueryNode;
}

View File

@@ -0,0 +1,39 @@
/// <reference types="./deduplicate-joins-transformer.d.ts" />
import { OperationNodeTransformer } from '../../operation-node/operation-node-transformer.js';
import { compare, freeze } from '../../util/object-utils.js';
export class DeduplicateJoinsTransformer extends OperationNodeTransformer {
transformSelectQuery(node, queryId) {
return this.#transformQuery(super.transformSelectQuery(node, queryId));
}
transformUpdateQuery(node, queryId) {
return this.#transformQuery(super.transformUpdateQuery(node, queryId));
}
transformDeleteQuery(node, queryId) {
return this.#transformQuery(super.transformDeleteQuery(node, queryId));
}
#transformQuery(node) {
if (!node.joins || node.joins.length === 0) {
return node;
}
return freeze({
...node,
joins: this.#deduplicateJoins(node.joins),
});
}
#deduplicateJoins(joins) {
const out = [];
for (let i = 0; i < joins.length; ++i) {
let foundDuplicate = false;
for (let j = 0; j < out.length; ++j) {
if (compare(joins[i], out[j])) {
foundDuplicate = true;
break;
}
}
if (!foundDuplicate) {
out.push(joins[i]);
}
}
return freeze(out);
}
}

View File

@@ -0,0 +1,211 @@
import type { QueryResult } from '../../driver/database-connection.js';
import type { RootOperationNode } from '../../query-compiler/query-compiler.js';
import type { KyselyPlugin, PluginTransformQueryArgs, PluginTransformResultArgs } from '../kysely-plugin.js';
import type { UnknownRow } from '../../util/type-utils.js';
import type { HandleEmptyInListsOptions } from './handle-empty-in-lists.js';
/**
* A plugin that allows handling `in ()` and `not in ()` expressions.
*
* These expressions are invalid SQL syntax for many databases, and result in runtime
* database errors.
*
* The workarounds used by other libraries always involve modifying the query under
* the hood, which is not aligned with Kysely's philosophy of WYSIWYG. We recommend manually checking
* for empty arrays before passing them as arguments to `in` and `not in` expressions
* instead, but understand that this can be cumbersome. Hence we're going with an
* opt-in approach where you can choose if and how to handle these cases. We do
* not want to make this the default behavior, as it can lead to unexpected behavior.
* Use it at your own risk. Test it. Make sure it works as expected for you.
*
* Using this plugin also allows you to throw an error (thus avoiding unnecessary
* requests to the database) or print a warning in these cases.
*
* ### Examples
*
* The following strategy replaces the `in`/`not in` expression with a noncontingent
* expression. A contradiction (falsy) `1 = 0` for `in`, and a tautology (truthy) `1 = 1` for `not in`),
* similarily to how {@link https://github.com/knex/knex/blob/176151d8048b2a7feeb89a3d649a5580786d4f4e/docs/src/guide/query-builder.md#L1763 | Knex.js},
* {@link https://github.com/prisma/prisma-engines/blob/99168c54187178484dae45d9478aa40cfd1866d2/quaint/src/visitor.rs#L804-L823 | PrismaORM},
* {@link https://github.com/laravel/framework/blob/8.x/src/Illuminate/Database/Query/Grammars/Grammar.php#L284-L291 | Laravel},
* {@link https://docs.sqlalchemy.org/en/13/core/engines.html#sqlalchemy.create_engine.params.empty_in_strategy | SQLAlchemy}
* handle this.
*
* ```ts
* import Sqlite from 'better-sqlite3'
* import {
* HandleEmptyInListsPlugin,
* Kysely,
* replaceWithNoncontingentExpression,
* SqliteDialect,
* } from 'kysely'
* import type { Database } from 'type-editor' // imaginary module
*
* const db = new Kysely<Database>({
* dialect: new SqliteDialect({
* database: new Sqlite(':memory:'),
* }),
* plugins: [
* new HandleEmptyInListsPlugin({
* strategy: replaceWithNoncontingentExpression
* })
* ],
* })
*
* const results = await db
* .selectFrom('person')
* .where('id', 'in', [])
* .where('first_name', 'not in', [])
* .selectAll()
* .execute()
* ```
*
* The generated SQL (SQLite):
*
* ```sql
* select * from "person" where 1 = 0 and 1 = 1
* ```
*
* The following strategy does the following:
*
* When `in`, pushes a `null` value into the empty list resulting in `in (null)`,
* similiarly to how {@link https://github.com/typeorm/typeorm/blob/0280cdc451c35ef73c830eb1191c95d34f6ce06e/src/query-builder/QueryBuilder.ts#L919-L922 | TypeORM}
* and {@link https://github.com/sequelize/sequelize/blob/0f2891c6897e12bf9bf56df344aae5b698f58c7d/packages/core/src/abstract-dialect/where-sql-builder.ts#L368-L379 | Sequelize}
* handle `in ()`. `in (null)` is logically the equivalent of `= null`, which returns
* `null`, which is a falsy expression in most SQL databases. We recommend NOT
* using this strategy if you plan to use `in` in `select`, `returning`, or `output`
* clauses, as the return type differs from the `SqlBool` default type for comparisons.
*
* When `not in`, casts the left operand as `char` and pushes a unique value into
* the empty list resulting in `cast({{lhs}} as char) not in ({{VALUE}})`. Casting
* is required to avoid database errors with non-string values.
*
* ```ts
* import Sqlite from 'better-sqlite3'
* import {
* HandleEmptyInListsPlugin,
* Kysely,
* pushValueIntoList,
* SqliteDialect
* } from 'kysely'
* import type { Database } from 'type-editor' // imaginary module
*
* const db = new Kysely<Database>({
* dialect: new SqliteDialect({
* database: new Sqlite(':memory:'),
* }),
* plugins: [
* new HandleEmptyInListsPlugin({
* strategy: pushValueIntoList('__kysely_no_values_were_provided__') // choose a unique value for not in. has to be something with zero chance being in the data.
* })
* ],
* })
*
* const results = await db
* .selectFrom('person')
* .where('id', 'in', [])
* .where('first_name', 'not in', [])
* .selectAll()
* .execute()
* ```
*
* The generated SQL (SQLite):
*
* ```sql
* select * from "person" where "id" in (null) and cast("first_name" as char) not in ('__kysely_no_values_were_provided__')
* ```
*
* The following custom strategy throws an error when an empty list is encountered
* to avoid unnecessary requests to the database:
*
* ```ts
* import Sqlite from 'better-sqlite3'
* import {
* HandleEmptyInListsPlugin,
* Kysely,
* SqliteDialect
* } from 'kysely'
* import type { Database } from 'type-editor' // imaginary module
*
* const db = new Kysely<Database>({
* dialect: new SqliteDialect({
* database: new Sqlite(':memory:'),
* }),
* plugins: [
* new HandleEmptyInListsPlugin({
* strategy: () => {
* throw new Error('Empty in/not-in is not allowed')
* }
* })
* ],
* })
*
* const results = await db
* .selectFrom('person')
* .where('id', 'in', [])
* .selectAll()
* .execute() // throws an error with 'Empty in/not-in is not allowed' message!
* ```
*/
export declare class HandleEmptyInListsPlugin implements KyselyPlugin {
#private;
readonly opt: HandleEmptyInListsOptions;
constructor(opt: HandleEmptyInListsOptions);
/**
* This is called for each query before it is executed. You can modify the query by
* transforming its {@link OperationNode} tree provided in {@link PluginTransformQueryArgs.node | args.node}
* and returning the transformed tree. You'd usually want to use an {@link OperationNodeTransformer}
* for this.
*
* If you need to pass some query-related data between this method and `transformResult` you
* can use a `WeakMap` with {@link PluginTransformQueryArgs.queryId | args.queryId} as the key:
*
* ```ts
* import type {
* KyselyPlugin,
* QueryResult,
* RootOperationNode,
* UnknownRow
* } from 'kysely'
*
* interface MyData {
* // ...
* }
* const data = new WeakMap<any, MyData>()
*
* const plugin = {
* transformQuery(args: PluginTransformQueryArgs): RootOperationNode {
* const something: MyData = {}
*
* // ...
*
* data.set(args.queryId, something)
*
* // ...
*
* return args.node
* },
*
* async transformResult(args: PluginTransformResultArgs): Promise<QueryResult<UnknownRow>> {
* // ...
*
* const something = data.get(args.queryId)
*
* // ...
*
* return args.result
* }
* } satisfies KyselyPlugin
* ```
*
* You should use a `WeakMap` instead of a `Map` or some other strong references because `transformQuery`
* is not always matched by a call to `transformResult` which would leave orphaned items in the map
* and cause a memory leak.
*/
transformQuery(args: PluginTransformQueryArgs): RootOperationNode;
/**
* This method is called for each query after it has been executed. The result
* of the query can be accessed through {@link PluginTransformResultArgs.result | args.result}.
* You can modify the result and return the modifier result.
*/
transformResult(args: PluginTransformResultArgs): Promise<QueryResult<UnknownRow>>;
}

View File

@@ -0,0 +1,159 @@
/// <reference types="./handle-empty-in-lists-plugin.d.ts" />
import { HandleEmptyInListsTransformer } from './handle-empty-in-lists-transformer.js';
/**
* A plugin that allows handling `in ()` and `not in ()` expressions.
*
* These expressions are invalid SQL syntax for many databases, and result in runtime
* database errors.
*
* The workarounds used by other libraries always involve modifying the query under
* the hood, which is not aligned with Kysely's philosophy of WYSIWYG. We recommend manually checking
* for empty arrays before passing them as arguments to `in` and `not in` expressions
* instead, but understand that this can be cumbersome. Hence we're going with an
* opt-in approach where you can choose if and how to handle these cases. We do
* not want to make this the default behavior, as it can lead to unexpected behavior.
* Use it at your own risk. Test it. Make sure it works as expected for you.
*
* Using this plugin also allows you to throw an error (thus avoiding unnecessary
* requests to the database) or print a warning in these cases.
*
* ### Examples
*
* The following strategy replaces the `in`/`not in` expression with a noncontingent
* expression. A contradiction (falsy) `1 = 0` for `in`, and a tautology (truthy) `1 = 1` for `not in`),
* similarily to how {@link https://github.com/knex/knex/blob/176151d8048b2a7feeb89a3d649a5580786d4f4e/docs/src/guide/query-builder.md#L1763 | Knex.js},
* {@link https://github.com/prisma/prisma-engines/blob/99168c54187178484dae45d9478aa40cfd1866d2/quaint/src/visitor.rs#L804-L823 | PrismaORM},
* {@link https://github.com/laravel/framework/blob/8.x/src/Illuminate/Database/Query/Grammars/Grammar.php#L284-L291 | Laravel},
* {@link https://docs.sqlalchemy.org/en/13/core/engines.html#sqlalchemy.create_engine.params.empty_in_strategy | SQLAlchemy}
* handle this.
*
* ```ts
* import Sqlite from 'better-sqlite3'
* import {
* HandleEmptyInListsPlugin,
* Kysely,
* replaceWithNoncontingentExpression,
* SqliteDialect,
* } from 'kysely'
* import type { Database } from 'type-editor' // imaginary module
*
* const db = new Kysely<Database>({
* dialect: new SqliteDialect({
* database: new Sqlite(':memory:'),
* }),
* plugins: [
* new HandleEmptyInListsPlugin({
* strategy: replaceWithNoncontingentExpression
* })
* ],
* })
*
* const results = await db
* .selectFrom('person')
* .where('id', 'in', [])
* .where('first_name', 'not in', [])
* .selectAll()
* .execute()
* ```
*
* The generated SQL (SQLite):
*
* ```sql
* select * from "person" where 1 = 0 and 1 = 1
* ```
*
* The following strategy does the following:
*
* When `in`, pushes a `null` value into the empty list resulting in `in (null)`,
* similiarly to how {@link https://github.com/typeorm/typeorm/blob/0280cdc451c35ef73c830eb1191c95d34f6ce06e/src/query-builder/QueryBuilder.ts#L919-L922 | TypeORM}
* and {@link https://github.com/sequelize/sequelize/blob/0f2891c6897e12bf9bf56df344aae5b698f58c7d/packages/core/src/abstract-dialect/where-sql-builder.ts#L368-L379 | Sequelize}
* handle `in ()`. `in (null)` is logically the equivalent of `= null`, which returns
* `null`, which is a falsy expression in most SQL databases. We recommend NOT
* using this strategy if you plan to use `in` in `select`, `returning`, or `output`
* clauses, as the return type differs from the `SqlBool` default type for comparisons.
*
* When `not in`, casts the left operand as `char` and pushes a unique value into
* the empty list resulting in `cast({{lhs}} as char) not in ({{VALUE}})`. Casting
* is required to avoid database errors with non-string values.
*
* ```ts
* import Sqlite from 'better-sqlite3'
* import {
* HandleEmptyInListsPlugin,
* Kysely,
* pushValueIntoList,
* SqliteDialect
* } from 'kysely'
* import type { Database } from 'type-editor' // imaginary module
*
* const db = new Kysely<Database>({
* dialect: new SqliteDialect({
* database: new Sqlite(':memory:'),
* }),
* plugins: [
* new HandleEmptyInListsPlugin({
* strategy: pushValueIntoList('__kysely_no_values_were_provided__') // choose a unique value for not in. has to be something with zero chance being in the data.
* })
* ],
* })
*
* const results = await db
* .selectFrom('person')
* .where('id', 'in', [])
* .where('first_name', 'not in', [])
* .selectAll()
* .execute()
* ```
*
* The generated SQL (SQLite):
*
* ```sql
* select * from "person" where "id" in (null) and cast("first_name" as char) not in ('__kysely_no_values_were_provided__')
* ```
*
* The following custom strategy throws an error when an empty list is encountered
* to avoid unnecessary requests to the database:
*
* ```ts
* import Sqlite from 'better-sqlite3'
* import {
* HandleEmptyInListsPlugin,
* Kysely,
* SqliteDialect
* } from 'kysely'
* import type { Database } from 'type-editor' // imaginary module
*
* const db = new Kysely<Database>({
* dialect: new SqliteDialect({
* database: new Sqlite(':memory:'),
* }),
* plugins: [
* new HandleEmptyInListsPlugin({
* strategy: () => {
* throw new Error('Empty in/not-in is not allowed')
* }
* })
* ],
* })
*
* const results = await db
* .selectFrom('person')
* .where('id', 'in', [])
* .selectAll()
* .execute() // throws an error with 'Empty in/not-in is not allowed' message!
* ```
*/
export class HandleEmptyInListsPlugin {
opt;
#transformer;
constructor(opt) {
this.opt = opt;
this.#transformer = new HandleEmptyInListsTransformer(opt.strategy);
}
transformQuery(args) {
return this.#transformer.transformNode(args.node, args.queryId);
}
async transformResult(args) {
return args.result;
}
}

View File

@@ -0,0 +1,8 @@
import type { BinaryOperationNode } from '../../operation-node/binary-operation-node.js';
import { OperationNodeTransformer } from '../../operation-node/operation-node-transformer.js';
import type { EmptyInListsStrategy } from './handle-empty-in-lists.js';
export declare class HandleEmptyInListsTransformer extends OperationNodeTransformer {
#private;
constructor(strategy: EmptyInListsStrategy);
protected transformBinaryOperation(node: BinaryOperationNode): BinaryOperationNode;
}

View File

@@ -0,0 +1,26 @@
/// <reference types="./handle-empty-in-lists-transformer.d.ts" />
import { OperationNodeTransformer } from '../../operation-node/operation-node-transformer.js';
import { PrimitiveValueListNode } from '../../operation-node/primitive-value-list-node.js';
import { OperatorNode } from '../../operation-node/operator-node.js';
import { ValueListNode } from '../../operation-node/value-list-node.js';
export class HandleEmptyInListsTransformer extends OperationNodeTransformer {
#strategy;
constructor(strategy) {
super();
this.#strategy = strategy;
}
transformBinaryOperation(node) {
if (this.#isEmptyInListNode(node)) {
return this.#strategy(node);
}
return node;
}
#isEmptyInListNode(node) {
const { operator, rightOperand } = node;
return ((PrimitiveValueListNode.is(rightOperand) ||
ValueListNode.is(rightOperand)) &&
rightOperand.values.length === 0 &&
OperatorNode.is(operator) &&
(operator.operator === 'in' || operator.operator === 'not in'));
}
}

View File

@@ -0,0 +1,44 @@
import { BinaryOperationNode } from '../../operation-node/binary-operation-node.js';
import { OperatorNode } from '../../operation-node/operator-node.js';
import type { PrimitiveValueListNode } from '../../operation-node/primitive-value-list-node.js';
import { ValueListNode } from '../../operation-node/value-list-node.js';
export interface HandleEmptyInListsOptions {
/**
* The strategy to use when handling `in ()` and `not in ()`.
*
* See {@link HandleEmptyInListsPlugin} for examples.
*/
strategy: EmptyInListsStrategy;
}
export type EmptyInListNode = BinaryOperationNode & {
operator: OperatorNode & {
operator: 'in' | 'not in';
};
rightOperand: (ValueListNode | PrimitiveValueListNode) & {
values: Readonly<[]>;
};
};
export type EmptyInListsStrategy = (node: EmptyInListNode) => BinaryOperationNode;
/**
* Replaces the `in`/`not in` expression with a noncontingent expression (always true or always
* false) depending on the original operator.
*
* This is how Knex.js, PrismaORM, Laravel, and SQLAlchemy handle `in ()` and `not in ()`.
*
* See {@link pushValueIntoList} for an alternative strategy.
*/
export declare function replaceWithNoncontingentExpression(node: EmptyInListNode): BinaryOperationNode;
/**
* When `in`, pushes a `null` value into the list resulting in `in (null)`. This
* is how TypeORM and Sequelize handle `in ()`. `in (null)` is logically the equivalent
* of `= null`, which returns `null`, which is a falsy expression in most SQL databases.
* We recommend NOT using this strategy if you plan to use `in` in `select`, `returning`,
* or `output` clauses, as the return type differs from the `SqlBool` default type.
*
* When `not in`, casts the left operand as `char` and pushes a literal value into
* the list resulting in `cast({{lhs}} as char) not in ({{VALUE}})`. Casting
* is required to avoid database errors with non-string columns.
*
* See {@link replaceWithNoncontingentExpression} for an alternative strategy.
*/
export declare function pushValueIntoList(uniqueNotInLiteral: '__kysely_no_values_were_provided__' | (string & {})): EmptyInListsStrategy;

View File

@@ -0,0 +1,63 @@
/// <reference types="./handle-empty-in-lists.d.ts" />
import { BinaryOperationNode } from '../../operation-node/binary-operation-node.js';
import { CastNode } from '../../operation-node/cast-node.js';
import { DataTypeNode } from '../../operation-node/data-type-node.js';
import { OperatorNode } from '../../operation-node/operator-node.js';
import { ValueListNode } from '../../operation-node/value-list-node.js';
import { ValueNode } from '../../operation-node/value-node.js';
import { freeze } from '../../util/object-utils.js';
let contradiction;
let eq;
let one;
let tautology;
/**
* Replaces the `in`/`not in` expression with a noncontingent expression (always true or always
* false) depending on the original operator.
*
* This is how Knex.js, PrismaORM, Laravel, and SQLAlchemy handle `in ()` and `not in ()`.
*
* See {@link pushValueIntoList} for an alternative strategy.
*/
export function replaceWithNoncontingentExpression(node) {
const _one = (one ||= ValueNode.createImmediate(1));
const _eq = (eq ||= OperatorNode.create('='));
if (node.operator.operator === 'in') {
return (contradiction ||= BinaryOperationNode.create(_one, _eq, ValueNode.createImmediate(0)));
}
return (tautology ||= BinaryOperationNode.create(_one, _eq, _one));
}
let char;
let listNull;
let listVal;
/**
* When `in`, pushes a `null` value into the list resulting in `in (null)`. This
* is how TypeORM and Sequelize handle `in ()`. `in (null)` is logically the equivalent
* of `= null`, which returns `null`, which is a falsy expression in most SQL databases.
* We recommend NOT using this strategy if you plan to use `in` in `select`, `returning`,
* or `output` clauses, as the return type differs from the `SqlBool` default type.
*
* When `not in`, casts the left operand as `char` and pushes a literal value into
* the list resulting in `cast({{lhs}} as char) not in ({{VALUE}})`. Casting
* is required to avoid database errors with non-string columns.
*
* See {@link replaceWithNoncontingentExpression} for an alternative strategy.
*/
export function pushValueIntoList(uniqueNotInLiteral) {
return function pushValueIntoList(node) {
if (node.operator.operator === 'in') {
return freeze({
...node,
rightOperand: (listNull ||= ValueListNode.create([
ValueNode.createImmediate(null),
])),
});
}
return freeze({
...node,
leftOperand: CastNode.create(node.leftOperand, (char ||= DataTypeNode.create('char'))),
rightOperand: (listVal ||= ValueListNode.create([
ValueNode.createImmediate(uniqueNotInLiteral),
])),
});
};
}

View File

@@ -0,0 +1,73 @@
import type { QueryResult } from '../../driver/database-connection.js';
import type { RootOperationNode } from '../../query-compiler/query-compiler.js';
import type { UnknownRow } from '../../util/type-utils.js';
import type { KyselyPlugin, PluginTransformQueryArgs, PluginTransformResultArgs } from '../kysely-plugin.js';
/**
* Transforms all ValueNodes to immediate.
*
* WARNING! This should never be part of the public API. Users should never use this.
* This is an internal helper.
*
* @internal
*/
export declare class ImmediateValuePlugin implements KyselyPlugin {
#private;
/**
* This is called for each query before it is executed. You can modify the query by
* transforming its {@link OperationNode} tree provided in {@link PluginTransformQueryArgs.node | args.node}
* and returning the transformed tree. You'd usually want to use an {@link OperationNodeTransformer}
* for this.
*
* If you need to pass some query-related data between this method and `transformResult` you
* can use a `WeakMap` with {@link PluginTransformQueryArgs.queryId | args.queryId} as the key:
*
* ```ts
* import type {
* KyselyPlugin,
* QueryResult,
* RootOperationNode,
* UnknownRow
* } from 'kysely'
*
* interface MyData {
* // ...
* }
* const data = new WeakMap<any, MyData>()
*
* const plugin = {
* transformQuery(args: PluginTransformQueryArgs): RootOperationNode {
* const something: MyData = {}
*
* // ...
*
* data.set(args.queryId, something)
*
* // ...
*
* return args.node
* },
*
* async transformResult(args: PluginTransformResultArgs): Promise<QueryResult<UnknownRow>> {
* // ...
*
* const something = data.get(args.queryId)
*
* // ...
*
* return args.result
* }
* } satisfies KyselyPlugin
* ```
*
* You should use a `WeakMap` instead of a `Map` or some other strong references because `transformQuery`
* is not always matched by a call to `transformResult` which would leave orphaned items in the map
* and cause a memory leak.
*/
transformQuery(args: PluginTransformQueryArgs): RootOperationNode;
/**
* This method is called for each query after it has been executed. The result
* of the query can be accessed through {@link PluginTransformResultArgs.result | args.result}.
* You can modify the result and return the modifier result.
*/
transformResult(args: PluginTransformResultArgs): Promise<QueryResult<UnknownRow>>;
}

View File

@@ -0,0 +1,19 @@
/// <reference types="./immediate-value-plugin.d.ts" />
import { ImmediateValueTransformer } from './immediate-value-transformer.js';
/**
* Transforms all ValueNodes to immediate.
*
* WARNING! This should never be part of the public API. Users should never use this.
* This is an internal helper.
*
* @internal
*/
export class ImmediateValuePlugin {
#transformer = new ImmediateValueTransformer();
transformQuery(args) {
return this.#transformer.transformNode(args.node, args.queryId);
}
transformResult(args) {
return Promise.resolve(args.result);
}
}

View File

@@ -0,0 +1,15 @@
import { OperationNodeTransformer } from '../../operation-node/operation-node-transformer.js';
import type { PrimitiveValueListNode } from '../../operation-node/primitive-value-list-node.js';
import { ValueNode } from '../../operation-node/value-node.js';
/**
* Transforms all ValueNodes to immediate.
*
* WARNING! This should never be part of the public API. Users should never use this.
* This is an internal helper.
*
* @internal
*/
export declare class ImmediateValueTransformer extends OperationNodeTransformer {
transformPrimitiveValueList(node: PrimitiveValueListNode): PrimitiveValueListNode;
transformValue(node: ValueNode): ValueNode;
}

View File

@@ -0,0 +1,20 @@
/// <reference types="./immediate-value-transformer.d.ts" />
import { OperationNodeTransformer } from '../../operation-node/operation-node-transformer.js';
import { ValueListNode } from '../../operation-node/value-list-node.js';
import { ValueNode } from '../../operation-node/value-node.js';
/**
* Transforms all ValueNodes to immediate.
*
* WARNING! This should never be part of the public API. Users should never use this.
* This is an internal helper.
*
* @internal
*/
export class ImmediateValueTransformer extends OperationNodeTransformer {
transformPrimitiveValueList(node) {
return ValueListNode.create(node.values.map(ValueNode.createImmediate));
}
transformValue(node) {
return ValueNode.createImmediate(node.value);
}
}

72
node_modules/kysely/dist/esm/plugin/kysely-plugin.d.ts generated vendored Normal file
View File

@@ -0,0 +1,72 @@
import type { QueryResult } from '../driver/database-connection.js';
import type { RootOperationNode } from '../query-compiler/query-compiler.js';
import type { QueryId } from '../util/query-id.js';
import type { UnknownRow } from '../util/type-utils.js';
export interface KyselyPlugin {
/**
* This is called for each query before it is executed. You can modify the query by
* transforming its {@link OperationNode} tree provided in {@link PluginTransformQueryArgs.node | args.node}
* and returning the transformed tree. You'd usually want to use an {@link OperationNodeTransformer}
* for this.
*
* If you need to pass some query-related data between this method and `transformResult` you
* can use a `WeakMap` with {@link PluginTransformQueryArgs.queryId | args.queryId} as the key:
*
* ```ts
* import type {
* KyselyPlugin,
* QueryResult,
* RootOperationNode,
* UnknownRow
* } from 'kysely'
*
* interface MyData {
* // ...
* }
* const data = new WeakMap<any, MyData>()
*
* const plugin = {
* transformQuery(args: PluginTransformQueryArgs): RootOperationNode {
* const something: MyData = {}
*
* // ...
*
* data.set(args.queryId, something)
*
* // ...
*
* return args.node
* },
*
* async transformResult(args: PluginTransformResultArgs): Promise<QueryResult<UnknownRow>> {
* // ...
*
* const something = data.get(args.queryId)
*
* // ...
*
* return args.result
* }
* } satisfies KyselyPlugin
* ```
*
* You should use a `WeakMap` instead of a `Map` or some other strong references because `transformQuery`
* is not always matched by a call to `transformResult` which would leave orphaned items in the map
* and cause a memory leak.
*/
transformQuery(args: PluginTransformQueryArgs): RootOperationNode;
/**
* This method is called for each query after it has been executed. The result
* of the query can be accessed through {@link PluginTransformResultArgs.result | args.result}.
* You can modify the result and return the modifier result.
*/
transformResult(args: PluginTransformResultArgs): Promise<QueryResult<UnknownRow>>;
}
export interface PluginTransformQueryArgs {
readonly queryId: QueryId;
readonly node: RootOperationNode;
}
export interface PluginTransformResultArgs {
readonly queryId: QueryId;
readonly result: QueryResult<UnknownRow>;
}

2
node_modules/kysely/dist/esm/plugin/kysely-plugin.js generated vendored Normal file
View File

@@ -0,0 +1,2 @@
/// <reference types="./kysely-plugin.d.ts" />
export {};

64
node_modules/kysely/dist/esm/plugin/noop-plugin.d.ts generated vendored Normal file
View File

@@ -0,0 +1,64 @@
import type { QueryResult } from '../driver/database-connection.js';
import type { RootOperationNode } from '../query-compiler/query-compiler.js';
import type { UnknownRow } from '../util/type-utils.js';
import type { KyselyPlugin, PluginTransformQueryArgs, PluginTransformResultArgs } from './kysely-plugin.js';
export declare class NoopPlugin implements KyselyPlugin {
/**
* This is called for each query before it is executed. You can modify the query by
* transforming its {@link OperationNode} tree provided in {@link PluginTransformQueryArgs.node | args.node}
* and returning the transformed tree. You'd usually want to use an {@link OperationNodeTransformer}
* for this.
*
* If you need to pass some query-related data between this method and `transformResult` you
* can use a `WeakMap` with {@link PluginTransformQueryArgs.queryId | args.queryId} as the key:
*
* ```ts
* import type {
* KyselyPlugin,
* QueryResult,
* RootOperationNode,
* UnknownRow
* } from 'kysely'
*
* interface MyData {
* // ...
* }
* const data = new WeakMap<any, MyData>()
*
* const plugin = {
* transformQuery(args: PluginTransformQueryArgs): RootOperationNode {
* const something: MyData = {}
*
* // ...
*
* data.set(args.queryId, something)
*
* // ...
*
* return args.node
* },
*
* async transformResult(args: PluginTransformResultArgs): Promise<QueryResult<UnknownRow>> {
* // ...
*
* const something = data.get(args.queryId)
*
* // ...
*
* return args.result
* }
* } satisfies KyselyPlugin
* ```
*
* You should use a `WeakMap` instead of a `Map` or some other strong references because `transformQuery`
* is not always matched by a call to `transformResult` which would leave orphaned items in the map
* and cause a memory leak.
*/
transformQuery(args: PluginTransformQueryArgs): RootOperationNode;
/**
* This method is called for each query after it has been executed. The result
* of the query can be accessed through {@link PluginTransformResultArgs.result | args.result}.
* You can modify the result and return the modifier result.
*/
transformResult(args: PluginTransformResultArgs): Promise<QueryResult<UnknownRow>>;
}

9
node_modules/kysely/dist/esm/plugin/noop-plugin.js generated vendored Normal file
View File

@@ -0,0 +1,9 @@
/// <reference types="./noop-plugin.d.ts" />
export class NoopPlugin {
transformQuery(args) {
return args.node;
}
async transformResult(args) {
return args.result;
}
}

View File

@@ -0,0 +1,126 @@
import type { QueryResult } from '../../driver/database-connection.js';
import type { RootOperationNode } from '../../query-compiler/query-compiler.js';
import type { UnknownRow } from '../../util/type-utils.js';
import type { KyselyPlugin, PluginTransformQueryArgs, PluginTransformResultArgs } from '../kysely-plugin.js';
export interface ParseJSONResultsPluginOptions {
/**
* When `'in-place'`, arrays' and objects' values are parsed in-place. This is
* the most time and space efficient option.
*
* This can result in runtime errors if some objects/arrays are readonly.
*
* When `'create'`, new arrays and objects are created to avoid such errors.
*
* Defaults to `'in-place'`.
*/
objectStrategy?: ObjectStrategy;
}
type ObjectStrategy = 'in-place' | 'create';
/**
* Parses JSON strings in query results into JSON objects.
*
* This plugin can be useful with dialects that don't automatically parse
* JSON into objects and arrays but return JSON strings instead.
*
* To apply this plugin globally, pass an instance of it to the `plugins` option
* when creating a new `Kysely` instance:
*
* ```ts
* import * as Sqlite from 'better-sqlite3'
* import { Kysely, ParseJSONResultsPlugin, SqliteDialect } from 'kysely'
* import type { Database } from 'type-editor' // imaginary module
*
* const db = new Kysely<Database>({
* dialect: new SqliteDialect({
* database: new Sqlite(':memory:'),
* }),
* plugins: [new ParseJSONResultsPlugin()],
* })
* ```
*
* To apply this plugin to a single query:
*
* ```ts
* import { ParseJSONResultsPlugin } from 'kysely'
* import { jsonArrayFrom } from 'kysely/helpers/sqlite'
*
* const result = await db
* .selectFrom('person')
* .select((eb) => [
* 'id',
* 'first_name',
* 'last_name',
* jsonArrayFrom(
* eb.selectFrom('pet')
* .whereRef('owner_id', '=', 'person.id')
* .select(['name', 'species'])
* ).as('pets')
* ])
* .withPlugin(new ParseJSONResultsPlugin())
* .execute()
* ```
*/
export declare class ParseJSONResultsPlugin implements KyselyPlugin {
#private;
readonly opt: ParseJSONResultsPluginOptions;
constructor(opt?: ParseJSONResultsPluginOptions);
/**
* This is called for each query before it is executed. You can modify the query by
* transforming its {@link OperationNode} tree provided in {@link PluginTransformQueryArgs.node | args.node}
* and returning the transformed tree. You'd usually want to use an {@link OperationNodeTransformer}
* for this.
*
* If you need to pass some query-related data between this method and `transformResult` you
* can use a `WeakMap` with {@link PluginTransformQueryArgs.queryId | args.queryId} as the key:
*
* ```ts
* import type {
* KyselyPlugin,
* QueryResult,
* RootOperationNode,
* UnknownRow
* } from 'kysely'
*
* interface MyData {
* // ...
* }
* const data = new WeakMap<any, MyData>()
*
* const plugin = {
* transformQuery(args: PluginTransformQueryArgs): RootOperationNode {
* const something: MyData = {}
*
* // ...
*
* data.set(args.queryId, something)
*
* // ...
*
* return args.node
* },
*
* async transformResult(args: PluginTransformResultArgs): Promise<QueryResult<UnknownRow>> {
* // ...
*
* const something = data.get(args.queryId)
*
* // ...
*
* return args.result
* }
* } satisfies KyselyPlugin
* ```
*
* You should use a `WeakMap` instead of a `Map` or some other strong references because `transformQuery`
* is not always matched by a call to `transformResult` which would leave orphaned items in the map
* and cause a memory leak.
*/
transformQuery(args: PluginTransformQueryArgs): RootOperationNode;
/**
* This method is called for each query after it has been executed. The result
* of the query can be accessed through {@link PluginTransformResultArgs.result | args.result}.
* You can modify the result and return the modifier result.
*/
transformResult(args: PluginTransformResultArgs): Promise<QueryResult<UnknownRow>>;
}
export {};

View File

@@ -0,0 +1,104 @@
/// <reference types="./parse-json-results-plugin.d.ts" />
import { isPlainObject, isString } from '../../util/object-utils.js';
/**
* Parses JSON strings in query results into JSON objects.
*
* This plugin can be useful with dialects that don't automatically parse
* JSON into objects and arrays but return JSON strings instead.
*
* To apply this plugin globally, pass an instance of it to the `plugins` option
* when creating a new `Kysely` instance:
*
* ```ts
* import * as Sqlite from 'better-sqlite3'
* import { Kysely, ParseJSONResultsPlugin, SqliteDialect } from 'kysely'
* import type { Database } from 'type-editor' // imaginary module
*
* const db = new Kysely<Database>({
* dialect: new SqliteDialect({
* database: new Sqlite(':memory:'),
* }),
* plugins: [new ParseJSONResultsPlugin()],
* })
* ```
*
* To apply this plugin to a single query:
*
* ```ts
* import { ParseJSONResultsPlugin } from 'kysely'
* import { jsonArrayFrom } from 'kysely/helpers/sqlite'
*
* const result = await db
* .selectFrom('person')
* .select((eb) => [
* 'id',
* 'first_name',
* 'last_name',
* jsonArrayFrom(
* eb.selectFrom('pet')
* .whereRef('owner_id', '=', 'person.id')
* .select(['name', 'species'])
* ).as('pets')
* ])
* .withPlugin(new ParseJSONResultsPlugin())
* .execute()
* ```
*/
export class ParseJSONResultsPlugin {
opt;
#objectStrategy;
constructor(opt = {}) {
this.opt = opt;
this.#objectStrategy = opt.objectStrategy || 'in-place';
}
// noop
transformQuery(args) {
return args.node;
}
async transformResult(args) {
return {
...args.result,
rows: parseArray(args.result.rows, this.#objectStrategy),
};
}
}
function parseArray(arr, objectStrategy) {
const target = objectStrategy === 'create' ? new Array(arr.length) : arr;
for (let i = 0; i < arr.length; ++i) {
target[i] = parse(arr[i], objectStrategy);
}
return target;
}
function parse(obj, objectStrategy) {
if (isString(obj)) {
return parseString(obj);
}
if (Array.isArray(obj)) {
return parseArray(obj, objectStrategy);
}
if (isPlainObject(obj)) {
return parseObject(obj, objectStrategy);
}
return obj;
}
function parseString(str) {
if (maybeJson(str)) {
try {
return parse(JSON.parse(str), 'in-place');
}
catch (err) {
// this catch block is intentionally empty.
}
}
return str;
}
function maybeJson(value) {
return value.match(/^[\[\{]/) != null;
}
function parseObject(obj, objectStrategy) {
const target = objectStrategy === 'create' ? {} : obj;
for (const key in obj) {
target[key] = parse(obj[key], objectStrategy);
}
return target;
}

View File

@@ -0,0 +1,66 @@
import type { QueryResult } from '../../driver/database-connection.js';
import type { RootOperationNode } from '../../query-compiler/query-compiler.js';
import type { KyselyPlugin, PluginTransformQueryArgs, PluginTransformResultArgs } from '../kysely-plugin.js';
import type { UnknownRow } from '../../util/type-utils.js';
export declare class WithSchemaPlugin implements KyselyPlugin {
#private;
constructor(schema: string);
/**
* This is called for each query before it is executed. You can modify the query by
* transforming its {@link OperationNode} tree provided in {@link PluginTransformQueryArgs.node | args.node}
* and returning the transformed tree. You'd usually want to use an {@link OperationNodeTransformer}
* for this.
*
* If you need to pass some query-related data between this method and `transformResult` you
* can use a `WeakMap` with {@link PluginTransformQueryArgs.queryId | args.queryId} as the key:
*
* ```ts
* import type {
* KyselyPlugin,
* QueryResult,
* RootOperationNode,
* UnknownRow
* } from 'kysely'
*
* interface MyData {
* // ...
* }
* const data = new WeakMap<any, MyData>()
*
* const plugin = {
* transformQuery(args: PluginTransformQueryArgs): RootOperationNode {
* const something: MyData = {}
*
* // ...
*
* data.set(args.queryId, something)
*
* // ...
*
* return args.node
* },
*
* async transformResult(args: PluginTransformResultArgs): Promise<QueryResult<UnknownRow>> {
* // ...
*
* const something = data.get(args.queryId)
*
* // ...
*
* return args.result
* }
* } satisfies KyselyPlugin
* ```
*
* You should use a `WeakMap` instead of a `Map` or some other strong references because `transformQuery`
* is not always matched by a call to `transformResult` which would leave orphaned items in the map
* and cause a memory leak.
*/
transformQuery(args: PluginTransformQueryArgs): RootOperationNode;
/**
* This method is called for each query after it has been executed. The result
* of the query can be accessed through {@link PluginTransformResultArgs.result | args.result}.
* You can modify the result and return the modifier result.
*/
transformResult(args: PluginTransformResultArgs): Promise<QueryResult<UnknownRow>>;
}

View File

@@ -0,0 +1,14 @@
/// <reference types="./with-schema-plugin.d.ts" />
import { WithSchemaTransformer } from './with-schema-transformer.js';
export class WithSchemaPlugin {
#transformer;
constructor(schema) {
this.#transformer = new WithSchemaTransformer(schema);
}
transformQuery(args) {
return this.#transformer.transformNode(args.node, args.queryId);
}
async transformResult(args) {
return args.result;
}
}

View File

@@ -0,0 +1,18 @@
import type { AggregateFunctionNode } from '../../operation-node/aggregate-function-node.js';
import type { FunctionNode } from '../../operation-node/function-node.js';
import { OperationNodeTransformer } from '../../operation-node/operation-node-transformer.js';
import type { OperationNode } from '../../operation-node/operation-node.js';
import type { ReferencesNode } from '../../operation-node/references-node.js';
import { SchemableIdentifierNode } from '../../operation-node/schemable-identifier-node.js';
import type { SelectModifierNode } from '../../operation-node/select-modifier-node.js';
import type { QueryId } from '../../util/query-id.js';
export declare class WithSchemaTransformer extends OperationNodeTransformer {
#private;
constructor(schema: string);
protected transformNodeImpl<T extends OperationNode>(node: T, queryId: QueryId): T;
protected transformSchemableIdentifier(node: SchemableIdentifierNode, queryId: QueryId): SchemableIdentifierNode;
protected transformReferences(node: ReferencesNode, queryId: QueryId): ReferencesNode;
protected transformAggregateFunction(node: AggregateFunctionNode, queryId: QueryId): AggregateFunctionNode;
protected transformFunction(node: FunctionNode, queryId: QueryId): FunctionNode;
protected transformSelectModifier(node: SelectModifierNode, queryId: QueryId): SelectModifierNode;
}

View File

@@ -0,0 +1,197 @@
/// <reference types="./with-schema-transformer.d.ts" />
import { AliasNode } from '../../operation-node/alias-node.js';
import { IdentifierNode } from '../../operation-node/identifier-node.js';
import { JoinNode } from '../../operation-node/join-node.js';
import { ListNode } from '../../operation-node/list-node.js';
import { OperationNodeTransformer } from '../../operation-node/operation-node-transformer.js';
import { SchemableIdentifierNode } from '../../operation-node/schemable-identifier-node.js';
import { TableNode } from '../../operation-node/table-node.js';
import { UsingNode } from '../../operation-node/using-node.js';
import { freeze } from '../../util/object-utils.js';
// This object exist only so that we get a type error when a new RootOperationNode
// is added. If you get a type error here, make sure to add the new root node and
// handle it correctly in the transformer.
//
// DO NOT REFACTOR THIS EVEN IF IT SEEMS USELESS TO YOU!
const ROOT_OPERATION_NODES = freeze({
AlterTableNode: true,
CreateIndexNode: true,
CreateSchemaNode: true,
CreateTableNode: true,
CreateTypeNode: true,
CreateViewNode: true,
RefreshMaterializedViewNode: true,
DeleteQueryNode: true,
DropIndexNode: true,
DropSchemaNode: true,
DropTableNode: true,
DropTypeNode: true,
DropViewNode: true,
InsertQueryNode: true,
RawNode: true,
SelectQueryNode: true,
UpdateQueryNode: true,
MergeQueryNode: true,
});
const SCHEMALESS_FUNCTIONS = {
json_agg: true,
to_json: true,
};
export class WithSchemaTransformer extends OperationNodeTransformer {
#schema;
#schemableIds = new Set();
#ctes = new Set();
constructor(schema) {
super();
this.#schema = schema;
}
transformNodeImpl(node, queryId) {
if (!this.#isRootOperationNode(node)) {
return super.transformNodeImpl(node, queryId);
}
const ctes = this.#collectCTEs(node);
for (const cte of ctes) {
this.#ctes.add(cte);
}
const tables = this.#collectSchemableIds(node);
for (const table of tables) {
this.#schemableIds.add(table);
}
const transformed = super.transformNodeImpl(node, queryId);
for (const table of tables) {
this.#schemableIds.delete(table);
}
for (const cte of ctes) {
this.#ctes.delete(cte);
}
return transformed;
}
transformSchemableIdentifier(node, queryId) {
const transformed = super.transformSchemableIdentifier(node, queryId);
if (transformed.schema || !this.#schemableIds.has(node.identifier.name)) {
return transformed;
}
return {
...transformed,
schema: IdentifierNode.create(this.#schema),
};
}
transformReferences(node, queryId) {
const transformed = super.transformReferences(node, queryId);
if (transformed.table.table.schema) {
return transformed;
}
return {
...transformed,
table: TableNode.createWithSchema(this.#schema, transformed.table.table.identifier.name),
};
}
transformAggregateFunction(node, queryId) {
return {
...super.transformAggregateFunction({ ...node, aggregated: [] }, queryId),
aggregated: this.#transformTableArgsWithoutSchemas(node, queryId, 'aggregated'),
};
}
transformFunction(node, queryId) {
return {
...super.transformFunction({ ...node, arguments: [] }, queryId),
arguments: this.#transformTableArgsWithoutSchemas(node, queryId, 'arguments'),
};
}
transformSelectModifier(node, queryId) {
return {
...super.transformSelectModifier({ ...node, of: undefined }, queryId),
of: node.of?.map((item) => TableNode.is(item) && !item.table.schema
? {
...item,
table: this.transformIdentifier(item.table.identifier, queryId),
}
: this.transformNode(item, queryId)),
};
}
#transformTableArgsWithoutSchemas(node, queryId, argsKey) {
return SCHEMALESS_FUNCTIONS[node.func]
? node[argsKey].map((arg) => !TableNode.is(arg) || arg.table.schema
? this.transformNode(arg, queryId)
: {
...arg,
table: this.transformIdentifier(arg.table.identifier, queryId),
})
: this.transformNodeList(node[argsKey], queryId);
}
#isRootOperationNode(node) {
return node.kind in ROOT_OPERATION_NODES;
}
#collectSchemableIds(node) {
const schemableIds = new Set();
if ('name' in node && node.name && SchemableIdentifierNode.is(node.name)) {
this.#collectSchemableId(node.name, schemableIds);
}
if ('from' in node && node.from) {
for (const from of node.from.froms) {
this.#collectSchemableIdsFromTableExpr(from, schemableIds);
}
}
if ('into' in node && node.into) {
this.#collectSchemableIdsFromTableExpr(node.into, schemableIds);
}
if ('table' in node && node.table) {
this.#collectSchemableIdsFromTableExpr(node.table, schemableIds);
}
if ('joins' in node && node.joins) {
for (const join of node.joins) {
this.#collectSchemableIdsFromTableExpr(join.table, schemableIds);
}
}
if ('using' in node && node.using) {
if (JoinNode.is(node.using)) {
this.#collectSchemableIdsFromTableExpr(node.using.table, schemableIds);
}
else {
this.#collectSchemableIdsFromTableExpr(node.using, schemableIds);
}
}
return schemableIds;
}
#collectCTEs(node) {
const ctes = new Set();
if ('with' in node && node.with) {
this.#collectCTEIds(node.with, ctes);
}
return ctes;
}
#collectSchemableIdsFromTableExpr(node, schemableIds) {
if (TableNode.is(node)) {
return this.#collectSchemableId(node.table, schemableIds);
}
if (AliasNode.is(node) && TableNode.is(node.node)) {
return this.#collectSchemableId(node.node.table, schemableIds);
}
if (ListNode.is(node)) {
for (const table of node.items) {
this.#collectSchemableIdsFromTableExpr(table, schemableIds);
}
return;
}
if (UsingNode.is(node)) {
for (const table of node.tables) {
this.#collectSchemableIdsFromTableExpr(table, schemableIds);
}
return;
}
}
#collectSchemableId(node, schemableIds) {
const id = node.identifier.name;
if (!this.#schemableIds.has(id) && !this.#ctes.has(id)) {
schemableIds.add(id);
}
}
#collectCTEIds(node, ctes) {
for (const expr of node.expressions) {
const cteId = expr.name.table.table.identifier.name;
if (!this.#ctes.has(cteId)) {
ctes.add(cteId);
}
}
}
}