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,80 @@
import type { Kysely } from '../../kysely.js';
import { DialectAdapterBase } from '../dialect-adapter-base.js';
import type { MigrationLockOptions } from '../dialect-adapter.js';
export declare class MysqlAdapter extends DialectAdapterBase {
/**
* Whether or not this dialect supports transactional DDL.
*
* If this is true, migrations are executed inside a transaction.
*/
get supportsTransactionalDdl(): boolean;
/**
* Whether or not this dialect supports the `returning` in inserts
* updates and deletes.
*/
get supportsReturning(): boolean;
/**
* This method is used to acquire a lock for the migrations so that
* it's not possible for two migration operations to run in parallel.
*
* Most dialects have explicit locks that can be used, like advisory locks
* in PostgreSQL and the get_lock function in MySQL.
*
* If the dialect doesn't have explicit locks the {@link MigrationLockOptions.lockTable}
* created by Kysely can be used instead. You can access it through the `options` object.
* The lock table has two columns `id` and `is_locked` and there's only one row in the table
* whose id is {@link MigrationLockOptions.lockRowId}. `is_locked` is an integer. Kysely
* takes care of creating the lock table and inserting the one single row to it before this
* method is executed. If the dialect supports schemas and the user has specified a custom
* schema in their migration settings, the options object also contains the schema name in
* {@link MigrationLockOptions.lockTableSchema}.
*
* Here's an example of how you might implement this method for a dialect that doesn't
* have explicit locks but supports `FOR UPDATE` row locks and transactional DDL:
*
* ```ts
* import { DialectAdapterBase, type MigrationLockOptions, Kysely } from 'kysely'
*
* export class MyAdapter extends DialectAdapterBase {
* override async acquireMigrationLock(
* db: Kysely<any>,
* options: MigrationLockOptions
* ): Promise<void> {
* const queryDb = options.lockTableSchema
* ? db.withSchema(options.lockTableSchema)
* : db
*
* // Since our imaginary dialect supports transactional DDL and has
* // row locks, we can simply take a row lock here and it will guarantee
* // all subsequent calls to this method from other transactions will
* // wait until this transaction finishes.
* await queryDb
* .selectFrom(options.lockTable)
* .selectAll()
* .where('id', '=', options.lockRowId)
* .forUpdate()
* .execute()
* }
*
* override async releaseMigrationLock() {
* // noop
* }
* }
* ```
*
* If `supportsTransactionalDdl` is `true` then the `db` passed to this method
* is a transaction inside which the migrations will be executed. Otherwise
* `db` is a single connection (session) that will be used to execute the
* migrations.
*/
acquireMigrationLock(db: Kysely<any>, _opt: MigrationLockOptions): Promise<void>;
/**
* Releases the migration lock. See {@link acquireMigrationLock}.
*
* If `supportsTransactionalDdl` is `true` then the `db` passed to this method
* is a transaction inside which the migrations were executed. Otherwise `db`
* is a single connection (session) that was used to execute the migrations
* and the `acquireMigrationLock` call.
*/
releaseMigrationLock(db: Kysely<any>, _opt: MigrationLockOptions): Promise<void>;
}

View File

@@ -0,0 +1,25 @@
/// <reference types="./mysql-adapter.d.ts" />
import { sql } from '../../raw-builder/sql.js';
import { DialectAdapterBase } from '../dialect-adapter-base.js';
const LOCK_ID = 'ea586330-2c93-47c8-908d-981d9d270f9d';
const LOCK_TIMEOUT_SECONDS = 60 * 60;
export class MysqlAdapter extends DialectAdapterBase {
get supportsTransactionalDdl() {
return false;
}
get supportsReturning() {
return false;
}
async acquireMigrationLock(db, _opt) {
// Kysely uses a single connection to run the migrations. Because of that, we
// can take a lock using `get_lock`. Locks acquired using `get_lock` get
// released when the connection is destroyed (session ends) or when the lock
// is released using `release_lock`. This way we know that the lock is either
// released by us after successfull or failed migrations OR it's released by
// MySQL if the process gets killed for some reason.
await sql `select get_lock(${sql.lit(LOCK_ID)}, ${sql.lit(LOCK_TIMEOUT_SECONDS)})`.execute(db);
}
async releaseMigrationLock(db, _opt) {
await sql `select release_lock(${sql.lit(LOCK_ID)})`.execute(db);
}
}

View File

@@ -0,0 +1,56 @@
import type { DatabaseConnection } from '../../driver/database-connection.js';
/**
* Config for the MySQL dialect.
*
* https://github.com/sidorares/node-mysql2#using-connection-pools
*/
export interface MysqlDialectConfig {
/**
* A mysql2 Pool instance or a function that returns one.
*
* If a function is provided, it's called once when the first query is executed.
*
* https://github.com/sidorares/node-mysql2#using-connection-pools
*/
pool: MysqlPool | (() => Promise<MysqlPool>);
/**
* Called once for each created connection.
*/
onCreateConnection?: (connection: DatabaseConnection) => Promise<void>;
/**
* Called every time a connection is acquired from the connection pool.
*/
onReserveConnection?: (connection: DatabaseConnection) => Promise<void>;
}
/**
* This interface is the subset of mysql2 driver's `Pool` class that
* kysely needs.
*
* We don't use the type from `mysql2` here to not have a dependency to it.
*
* https://github.com/sidorares/node-mysql2#using-connection-pools
*/
export interface MysqlPool {
getConnection(callback: (error: unknown, connection: MysqlPoolConnection) => void): void;
end(callback: (error: unknown) => void): void;
}
export interface MysqlPoolConnection {
query(sql: string, parameters: Array<unknown>): {
stream: <T>(options: MysqlStreamOptions) => MysqlStream<T>;
};
query(sql: string, parameters: Array<unknown>, callback: (error: unknown, result: MysqlQueryResult) => void): void;
release(): void;
}
export interface MysqlStreamOptions {
highWaterMark?: number;
objectMode?: true;
}
export interface MysqlStream<T> {
[Symbol.asyncIterator](): AsyncIterableIterator<T>;
}
export interface MysqlOkPacket {
affectedRows: number;
changedRows: number;
insertId: number;
}
export type MysqlQueryResult = MysqlOkPacket | Record<string, unknown>[];

View File

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

View File

@@ -0,0 +1,61 @@
import type { Driver } from '../../driver/driver.js';
import type { Kysely } from '../../kysely.js';
import type { QueryCompiler } from '../../query-compiler/query-compiler.js';
import type { Dialect } from '../dialect.js';
import type { DatabaseIntrospector } from '../database-introspector.js';
import type { DialectAdapter } from '../dialect-adapter.js';
import type { MysqlDialectConfig } from './mysql-dialect-config.js';
/**
* MySQL dialect that uses the [mysql2](https://github.com/sidorares/node-mysql2#readme) library.
*
* The constructor takes an instance of {@link MysqlDialectConfig}.
*
* ```ts
* import { createPool } from 'mysql2'
*
* new MysqlDialect({
* pool: createPool({
* database: 'some_db',
* host: 'localhost',
* })
* })
* ```
*
* If you want the pool to only be created once it's first used, `pool`
* can be a function:
*
* ```ts
* import { createPool } from 'mysql2'
*
* new MysqlDialect({
* pool: async () => createPool({
* database: 'some_db',
* host: 'localhost',
* })
* })
* ```
*/
export declare class MysqlDialect implements Dialect {
#private;
constructor(config: MysqlDialectConfig);
/**
* Creates a driver for the dialect.
*/
createDriver(): Driver;
/**
* Creates a query compiler for the dialect.
*/
createQueryCompiler(): QueryCompiler;
/**
* Creates an adapter for the dialect.
*/
createAdapter(): DialectAdapter;
/**
* Creates a database introspector that can be used to get database metadata
* such as the table names and column names of those tables.
*
* `db` never has any plugins installed. It's created using
* {@link Kysely.withoutPlugins}.
*/
createIntrospector(db: Kysely<any>): DatabaseIntrospector;
}

View File

@@ -0,0 +1,53 @@
/// <reference types="./mysql-dialect.d.ts" />
import { MysqlDriver } from './mysql-driver.js';
import { MysqlQueryCompiler } from './mysql-query-compiler.js';
import { MysqlIntrospector } from './mysql-introspector.js';
import { MysqlAdapter } from './mysql-adapter.js';
/**
* MySQL dialect that uses the [mysql2](https://github.com/sidorares/node-mysql2#readme) library.
*
* The constructor takes an instance of {@link MysqlDialectConfig}.
*
* ```ts
* import { createPool } from 'mysql2'
*
* new MysqlDialect({
* pool: createPool({
* database: 'some_db',
* host: 'localhost',
* })
* })
* ```
*
* If you want the pool to only be created once it's first used, `pool`
* can be a function:
*
* ```ts
* import { createPool } from 'mysql2'
*
* new MysqlDialect({
* pool: async () => createPool({
* database: 'some_db',
* host: 'localhost',
* })
* })
* ```
*/
export class MysqlDialect {
#config;
constructor(config) {
this.#config = config;
}
createDriver() {
return new MysqlDriver(this.#config);
}
createQueryCompiler() {
return new MysqlQueryCompiler();
}
createAdapter() {
return new MysqlAdapter();
}
createIntrospector(db) {
return new MysqlIntrospector(db);
}
}

View File

@@ -0,0 +1,52 @@
import type { DatabaseConnection, QueryResult } from '../../driver/database-connection.js';
import type { Driver, TransactionSettings } from '../../driver/driver.js';
import { CompiledQuery } from '../../query-compiler/compiled-query.js';
import type { QueryCompiler } from '../../query-compiler/query-compiler.js';
import type { MysqlDialectConfig, MysqlPoolConnection } from './mysql-dialect-config.js';
declare const PRIVATE_RELEASE_METHOD: unique symbol;
export declare class MysqlDriver implements Driver {
#private;
constructor(configOrPool: MysqlDialectConfig);
/**
* Initializes the driver.
*
* After calling this method the driver should be usable and `acquireConnection` etc.
* methods should be callable.
*/
init(): Promise<void>;
/**
* Acquires a new connection from the pool.
*/
acquireConnection(): Promise<DatabaseConnection>;
/**
* Begins a transaction.
*/
beginTransaction(connection: DatabaseConnection, settings: TransactionSettings): Promise<void>;
/**
* Commits a transaction.
*/
commitTransaction(connection: DatabaseConnection): Promise<void>;
/**
* Rolls back a transaction.
*/
rollbackTransaction(connection: DatabaseConnection): Promise<void>;
savepoint(connection: DatabaseConnection, savepointName: string, compileQuery: QueryCompiler['compileQuery']): Promise<void>;
rollbackToSavepoint(connection: DatabaseConnection, savepointName: string, compileQuery: QueryCompiler['compileQuery']): Promise<void>;
releaseSavepoint(connection: DatabaseConnection, savepointName: string, compileQuery: QueryCompiler['compileQuery']): Promise<void>;
/**
* Releases a connection back to the pool.
*/
releaseConnection(connection: MysqlConnection): Promise<void>;
/**
* Destroys the driver and releases all resources.
*/
destroy(): Promise<void>;
}
declare class MysqlConnection implements DatabaseConnection {
#private;
constructor(rawConnection: MysqlPoolConnection);
executeQuery<O>(compiledQuery: CompiledQuery): Promise<QueryResult<O>>;
streamQuery<O>(compiledQuery: CompiledQuery, _chunkSize: number): AsyncIterableIterator<QueryResult<O>>;
[PRIVATE_RELEASE_METHOD](): void;
}
export {};

View File

@@ -0,0 +1,177 @@
/// <reference types="./mysql-driver.d.ts" />
import { parseSavepointCommand } from '../../parser/savepoint-parser.js';
import { CompiledQuery } from '../../query-compiler/compiled-query.js';
import { isFunction, isObject, freeze } from '../../util/object-utils.js';
import { createQueryId } from '../../util/query-id.js';
import { extendStackTrace } from '../../util/stack-trace-utils.js';
const PRIVATE_RELEASE_METHOD = Symbol();
export class MysqlDriver {
#config;
#connections = new WeakMap();
#pool;
constructor(configOrPool) {
this.#config = freeze({ ...configOrPool });
}
async init() {
this.#pool = isFunction(this.#config.pool)
? await this.#config.pool()
: this.#config.pool;
}
async acquireConnection() {
const rawConnection = await this.#acquireConnection();
let connection = this.#connections.get(rawConnection);
if (!connection) {
connection = new MysqlConnection(rawConnection);
this.#connections.set(rawConnection, connection);
// The driver must take care of calling `onCreateConnection` when a new
// connection is created. The `mysql2` module doesn't provide an async hook
// for the connection creation. We need to call the method explicitly.
if (this.#config?.onCreateConnection) {
await this.#config.onCreateConnection(connection);
}
}
if (this.#config?.onReserveConnection) {
await this.#config.onReserveConnection(connection);
}
return connection;
}
async #acquireConnection() {
return new Promise((resolve, reject) => {
this.#pool.getConnection(async (err, rawConnection) => {
if (err) {
reject(err);
}
else {
resolve(rawConnection);
}
});
});
}
async beginTransaction(connection, settings) {
if (settings.isolationLevel || settings.accessMode) {
const parts = [];
if (settings.isolationLevel) {
parts.push(`isolation level ${settings.isolationLevel}`);
}
if (settings.accessMode) {
parts.push(settings.accessMode);
}
const sql = `set transaction ${parts.join(', ')}`;
// On MySQL this sets the isolation level of the next transaction.
await connection.executeQuery(CompiledQuery.raw(sql));
}
await connection.executeQuery(CompiledQuery.raw('begin'));
}
async commitTransaction(connection) {
await connection.executeQuery(CompiledQuery.raw('commit'));
}
async rollbackTransaction(connection) {
await connection.executeQuery(CompiledQuery.raw('rollback'));
}
async savepoint(connection, savepointName, compileQuery) {
await connection.executeQuery(compileQuery(parseSavepointCommand('savepoint', savepointName), createQueryId()));
}
async rollbackToSavepoint(connection, savepointName, compileQuery) {
await connection.executeQuery(compileQuery(parseSavepointCommand('rollback to', savepointName), createQueryId()));
}
async releaseSavepoint(connection, savepointName, compileQuery) {
await connection.executeQuery(compileQuery(parseSavepointCommand('release savepoint', savepointName), createQueryId()));
}
async releaseConnection(connection) {
connection[PRIVATE_RELEASE_METHOD]();
}
async destroy() {
return new Promise((resolve, reject) => {
this.#pool.end((err) => {
if (err) {
reject(err);
}
else {
resolve();
}
});
});
}
}
function isOkPacket(obj) {
return isObject(obj) && 'insertId' in obj && 'affectedRows' in obj;
}
class MysqlConnection {
#rawConnection;
constructor(rawConnection) {
this.#rawConnection = rawConnection;
}
async executeQuery(compiledQuery) {
try {
const result = await this.#executeQuery(compiledQuery);
if (isOkPacket(result)) {
const { insertId, affectedRows, changedRows } = result;
return {
insertId: insertId !== undefined &&
insertId !== null &&
insertId.toString() !== '0'
? BigInt(insertId)
: undefined,
numAffectedRows: affectedRows !== undefined && affectedRows !== null
? BigInt(affectedRows)
: undefined,
numChangedRows: changedRows !== undefined && changedRows !== null
? BigInt(changedRows)
: undefined,
rows: [],
};
}
else if (Array.isArray(result)) {
return {
rows: result,
};
}
return {
rows: [],
};
}
catch (err) {
throw extendStackTrace(err, new Error());
}
}
#executeQuery(compiledQuery) {
return new Promise((resolve, reject) => {
this.#rawConnection.query(compiledQuery.sql, compiledQuery.parameters, (err, result) => {
if (err) {
reject(err);
}
else {
resolve(result);
}
});
});
}
async *streamQuery(compiledQuery, _chunkSize) {
const stream = this.#rawConnection
.query(compiledQuery.sql, compiledQuery.parameters)
.stream({
objectMode: true,
});
try {
for await (const row of stream) {
yield {
rows: [row],
};
}
}
catch (ex) {
if (ex &&
typeof ex === 'object' &&
'code' in ex &&
// @ts-ignore
ex.code === 'ERR_STREAM_PREMATURE_CLOSE') {
// Most likely because of https://github.com/mysqljs/mysql/blob/master/lib/protocol/sequences/Query.js#L220
return;
}
throw ex;
}
}
[PRIVATE_RELEASE_METHOD]() {
this.#rawConnection.release();
}
}

View File

@@ -0,0 +1,20 @@
import type { DatabaseIntrospector, DatabaseMetadata, DatabaseMetadataOptions, SchemaMetadata, TableMetadata } from '../database-introspector.js';
import type { Kysely } from '../../kysely.js';
export declare class MysqlIntrospector implements DatabaseIntrospector {
#private;
constructor(db: Kysely<any>);
/**
* Get schema metadata.
*/
getSchemas(): Promise<SchemaMetadata[]>;
/**
* Get tables and views metadata.
*/
getTables(options?: DatabaseMetadataOptions): Promise<TableMetadata[]>;
/**
* Get the database metadata such as table and column names.
*
* @deprecated Use getTables() instead.
*/
getMetadata(options?: DatabaseMetadataOptions): Promise<DatabaseMetadata>;
}

View File

@@ -0,0 +1,76 @@
/// <reference types="./mysql-introspector.d.ts" />
import { DEFAULT_MIGRATION_LOCK_TABLE, DEFAULT_MIGRATION_TABLE, } from '../../migration/migrator.js';
import { freeze } from '../../util/object-utils.js';
import { sql } from '../../raw-builder/sql.js';
export class MysqlIntrospector {
#db;
constructor(db) {
this.#db = db;
}
async getSchemas() {
let rawSchemas = await this.#db
.selectFrom('information_schema.schemata')
.select('schema_name')
.$castTo()
.execute();
return rawSchemas.map((it) => ({ name: it.SCHEMA_NAME }));
}
async getTables(options = { withInternalKyselyTables: false }) {
let query = this.#db
.selectFrom('information_schema.columns as columns')
.innerJoin('information_schema.tables as tables', (b) => b
.onRef('columns.TABLE_CATALOG', '=', 'tables.TABLE_CATALOG')
.onRef('columns.TABLE_SCHEMA', '=', 'tables.TABLE_SCHEMA')
.onRef('columns.TABLE_NAME', '=', 'tables.TABLE_NAME'))
.select([
'columns.COLUMN_NAME',
'columns.COLUMN_DEFAULT',
'columns.TABLE_NAME',
'columns.TABLE_SCHEMA',
'tables.TABLE_TYPE',
'columns.IS_NULLABLE',
'columns.DATA_TYPE',
'columns.EXTRA',
'columns.COLUMN_COMMENT',
])
.where('columns.TABLE_SCHEMA', '=', sql `database()`)
.orderBy('columns.TABLE_NAME')
.orderBy('columns.ORDINAL_POSITION')
.$castTo();
if (!options.withInternalKyselyTables) {
query = query
.where('columns.TABLE_NAME', '!=', DEFAULT_MIGRATION_TABLE)
.where('columns.TABLE_NAME', '!=', DEFAULT_MIGRATION_LOCK_TABLE);
}
const rawColumns = await query.execute();
return this.#parseTableMetadata(rawColumns);
}
async getMetadata(options) {
return {
tables: await this.getTables(options),
};
}
#parseTableMetadata(columns) {
return columns.reduce((tables, it) => {
let table = tables.find((tbl) => tbl.name === it.TABLE_NAME);
if (!table) {
table = freeze({
name: it.TABLE_NAME,
isView: it.TABLE_TYPE === 'VIEW',
schema: it.TABLE_SCHEMA,
columns: [],
});
tables.push(table);
}
table.columns.push(freeze({
name: it.COLUMN_NAME,
dataType: it.DATA_TYPE,
isNullable: it.IS_NULLABLE === 'YES',
isAutoIncrementing: it.EXTRA.toLowerCase().includes('auto_increment'),
hasDefaultValue: it.COLUMN_DEFAULT !== null,
comment: it.COLUMN_COMMENT === '' ? undefined : it.COLUMN_COMMENT,
}));
return tables;
}, []);
}
}

View File

@@ -0,0 +1,13 @@
import type { CreateIndexNode } from '../../operation-node/create-index-node.js';
import { DefaultQueryCompiler } from '../../query-compiler/default-query-compiler.js';
export declare class MysqlQueryCompiler extends DefaultQueryCompiler {
protected getCurrentParameterPlaceholder(): string;
protected getLeftExplainOptionsWrapper(): string;
protected getExplainOptionAssignment(): string;
protected getExplainOptionsDelimiter(): string;
protected getRightExplainOptionsWrapper(): string;
protected getLeftIdentifierWrapper(): string;
protected getRightIdentifierWrapper(): string;
protected sanitizeIdentifier(identifier: string): string;
protected visitCreateIndex(node: CreateIndexNode): void;
}

View File

@@ -0,0 +1,57 @@
/// <reference types="./mysql-query-compiler.d.ts" />
import { DefaultQueryCompiler } from '../../query-compiler/default-query-compiler.js';
const ID_WRAP_REGEX = /`/g;
export class MysqlQueryCompiler extends DefaultQueryCompiler {
getCurrentParameterPlaceholder() {
return '?';
}
getLeftExplainOptionsWrapper() {
return '';
}
getExplainOptionAssignment() {
return '=';
}
getExplainOptionsDelimiter() {
return ' ';
}
getRightExplainOptionsWrapper() {
return '';
}
getLeftIdentifierWrapper() {
return '`';
}
getRightIdentifierWrapper() {
return '`';
}
sanitizeIdentifier(identifier) {
return identifier.replace(ID_WRAP_REGEX, '``');
}
visitCreateIndex(node) {
this.append('create ');
if (node.unique) {
this.append('unique ');
}
this.append('index ');
if (node.ifNotExists) {
this.append('if not exists ');
}
this.visitNode(node.name);
if (node.using) {
this.append(' using ');
this.visitNode(node.using);
}
if (node.table) {
this.append(' on ');
this.visitNode(node.table);
}
if (node.columns) {
this.append(' (');
this.compileList(node.columns);
this.append(')');
}
if (node.where) {
this.append(' ');
this.visitNode(node.where);
}
}
}