UNPKG

db-sql-toolkit

Version:

Helps with SQL statements, database migration, and bulk execution.

335 lines (250 loc) 10.4 kB
# db-sql-toolkit Helps with SQL statements, database migration, and bulk execution. ## Installation `npm install db-sql-toolkit` ## Usage ### sql The `sql` function is a tagged template function to write SQL statements and include parameters in the correct places. ```js const id = "1234"; const statement = sql` SELECT name , version , author FROM package WHERE id = ${id} `; ``` The `sql` function returns a tuple consisting of the statement and an array of passed parameters. In the returned statement all parameters are replaced by question marks (?). **Hint:** You can get syntax highlighting in VS Code by installing an extension: [es6-string-html](https://marketplace.visualstudio.com/items?itemName=Tobermory.es6-string-html) #### Nesting You can nest `sql` statements: ```js const where = sql` id = ${1234} `; const statement = sql` SELECT name , version , author FROM package WHERE ${where} `; ``` ### SQL literals If you want to use variables as SQL literals (where no variables are supported), you can use the `sqlLiteral` function: ```js const concatCharacter = "|"; const statement = sql` SELECT GROUP_CONCAT(name, '${sqlLiteral(concatCharacter)}') AS package_names FROM package GROUP BY author `; ``` This inserts the variable `concatCharacter` as a literal into the SQL statement. ### bulkInsertEntities Insert many entities in as few operations as possible. ```ts const packages: Package[] = [ // a lot of packages ] const getParameters = (package: Package): unknown[] => [ package.id, package.name, package.version, package.author, ]; const statement = sql` INSERT INTO package ( id , name , version , author ) VALUES (${getParameters}) `; await bulkInsertEntities(database, packages, statement); ``` This function (and all bulk* functions) uses the `MaxVariableNumber` property of the [Database](#database) and splits the operation into multiple operations, if required. **Important:** To use this function you have to pass the function to get the parameters for one entity as the only parameter into the `sql` function. This function supports executing multiple statements with the same data. For example, if you want to insert the same data into two different tables, you can do it like this: ```ts const packages: Package[] = [ // a lot of packages ]; const getParametersPackage = (package: Package): unknown[] => [ package.id, package.name, package.version, package.author, ]; const statementPackage = sql` INSERT INTO package ( id , name , version , author ) VALUES (${getParametersPackage}) `; const getParametersAuthor = (package: Package): unknown[] => [ package.version, ]; const statementAuthor = sql` INSERT INTO author ( author ) VALUES (${getParametersAuthor}) `; await bulkInsertEntities(database, packages, statementPackage, statementAuthor); ``` ### bulkExecuteCommand Executes a SQL statement in as few operations as possible. **Important:** To use this function one parameter of the `sql` function has to be an array. **Warning:** Do not use the `NOT IN` operator. If you do so and the operation cannot be executed in one run, you get wrong results. ### bulkGetRows Selects entities in as few operations as possible. ### bulkGetCount Gets the total number of entities in as few operations as possible. You have to select `COUNT(*)` in the SQL statement. ### applyMigrations With the help of the `applyMigrations` function you can execute database upgrades. It executes the required migrations in the given order. ```ts import { applyMigrations, Database } from "db-sql-toolkit"; async function upgradeDatabase(database: Database): Promise<void> { await applyMigrations({ database, migrations: [ { id: "initial", apply: createDatabase }, { id: "add-feature", apply: applyAddFeature }, { id: "some-bugfix", apply: applySomeBugfix }, ] }); } async function createDatabase(database: Database): Promise<void> { // Create the initial database. } ``` The `apply` function is called for each migration. The order of the migrations is important. The function `createDatabase` will be called first, then `applyAddFeature`, and finally `applySomeBugfix`. The `id` of the migration is used to check if the migration was already executed. If it was, the `apply` function will not be called again. The `id` of the migration can be any string. It is recommended to use a unique ID for each migration, e.g. a timestamp or a version number. Optionally you can pass a `targetId` parameter to the `applyMigrations` function. This will stop the execution of the migrations if the target ID is reached. If you omit this parameter, all migrations will be executed. ```ts import { applyMigrations, Database } from "db-sql-toolkit"; async function upgradeDatabase(database: Database): Promise<void> { await applyMigrations({ database, targetId: "add-feature", migrations: [ { id: "initial", apply: createDatabase }, { id: "add-feature", apply: applyAddFeature }, { id: "some-bugfix", apply: applySomeBugfix }, ] }); } ``` Here, the migration will stop after the `add-feature` migration. The `apply` function of the `some-bugfix` migration will not be called. This may be useful for testing migrations. See [Database](#database) for type information. By default it uses the `_db_migration` table (and creates it if needed) to store and update the IDs of the executed migrations. You can change this by passing your own `getExecutedMigrationIds` and `insertMigrationId` functions to the `applyMigrations` function: ```ts async function getExecutedMigrationIds(database: Database): Promise<string[]> { // Get the IDs of the executed migrations. return ["initial", "add-feature"]; } async function insertMigrationId(database: Database, id: string): Promise<void> { // Insert the migration ID into the database. } ``` Optionally you can pass a `writeLog` function to the `applyMigrations` function, e.g. to print the IDs of the executed migrations: ```ts function writeLog(message: string): void { // Log the message. } ``` ### migrate This is an alternative to the `applyMigrations` function. This function uses a `number` as the version of the database. With the help of the `migrate` function you can execute database upgrades. ```ts import { migrate, Database } from "db-sql-toolkit"; async function upgradeDatabase(database: Database): Promise<void> { await migrate({ database, // The targetVersion parameter is optional. If you omit it, the migrationMap will be executed until the last version. targetVersion: 3, migrationMap: [ [1, createDatabase], [2, updateToVersion2], [3, updateToVersion3], ] }); } async function createDatabase(database: Database): Promise<void> { // Create the initial database. } // updateToVersion2 and updateToVersion3 are omitted. ``` See [Database](#database) for type information. It calls all required upgrade functions in the correct order. In the example above, to upgrade the database from version 2 to 3, the function `updateToVersion3` will be called. By default it uses the `db_version` table (and creates it if needed) to store and update the current version of the database. You can change this by passing your own `getCurrentVersion` and `updateVersion` functions to the `migrate` function: ```ts async function getCurrentVersion(database: Database): Promise<number> { // Get the current version of the database. return 2; } async function updateVersion(database: Database, version: number): Promise<void> { // Write the new version into the database. } ``` Optionally you can pass a `writeLog` function to the `migrate` function, e.g. to print the current and updated version of the database: ```ts function writeLog(message: string): void { // Log the message. } ``` ## Types ### Database The `Database` type is defined as an interface: ```ts interface Database { MaxVariableNumber: number; executeSqlCommand: (statement: string, parameters: unknown[]) => Promise<void>; getRows: <T>(statement: string, parameters: unknown[]) => Promise<T[]>; } ``` `MaxVariableNumber` is the maximum number of parameters per SQL statement. ### SQL Statement Parameters The first type parameter of the `sql` function is a union type of all types that can be used as parameters in a SQL statement. To create a typed version of the `sql` function and all important types function, you can use the following type definitions: ```ts import { type AllowedSqlParams, type BulkExecuteStatementParams as BulkExecuteStatementParamsOriginal, type BulkExecuteStatementSqlFnParams, type BulkStatementParams as BulkStatementParamsOriginal, type BulkStatementSqlFnParams, type Database as DatabaseOriginal, type SqlReturnType, type StatementParams as StatementParamsOriginal, type StatementSqlFnParams, sql as sqlOriginal, } from "db-sql-toolkit"; // Your allowed parameter types. type DatabaseParam = string | number | boolean | null | undefined; interface Database extends DatabaseOriginal<DatabaseParam> {} type StatementParams = StatementParamsOriginal<DatabaseParam>; type BulkStatementParams<TData> = BulkStatementParamsOriginal<TData, DatabaseParam>; type BulkExecuteStatementParams = BulkExecuteStatementParamsOriginal<DatabaseParam>; function sql(strings: TemplateStringsArray, ...values: StatementSqlFnParams<DatabaseParam>): StatementParams; function sql<TData>(strings: TemplateStringsArray, ...values: BulkStatementSqlFnParams<TData, DatabaseParam>): BulkStatementParams<TData>; function sql(strings: TemplateStringsArray, ...values: BulkExecuteStatementSqlFnParams<DatabaseParam>): BulkExecuteStatementParams; function sql<TData>(strings: TemplateStringsArray, ...values: AllowedSqlParams<TData, DatabaseParam>): SqlReturnType<TData, DatabaseParam> { return sqlOriginal<DatabaseParam>(strings, ...values); } ``` The default type for the parameters is: ```ts type DefaultParamType = string | number | boolean | null; ```