Advanced usage
Custom booleans values
Sometimes, especially in Oracle databases, you need to represent a boolean with other values except true or false. For example, if your field in the database represents the true value with the char Y
and the false value with the char N
.
For example:
import { Table } from "ts-sql-query/Table";
import { CustomBooleanTypeAdapter } from "ts-sql-query/TypeAdapter";
const tCustomCompany = new class TCustomCompany extends Table<DBConnection, 'TCustomCompany'> {
id = this.autogeneratedPrimaryKey('id', 'int');
name = this.column('name', 'string');
isBig = this.column('is_big', 'boolean', new CustomBooleanTypeAdapter('Y', 'N'));
constructor() {
super('custom_company'); // table name in the database
}
}();
The table custom_company
the field is_big
accepts the values Y
and N
. This field represents a boolean type, and on the JavaScript side, it will be mapped as boolean. But, on the database side, the field will be treated with appropriated values. The conversion between values will be performed by ts-sql-query automatically; you don't need to be worried about the type mismatching even if you try to assign the value to another field with a different way of representing booleans.
You can perform an insert in this way:
const insertCustomCompany = connection.insertInto(tCustomCompany).set({
name: 'My Big Company',
isBig: true
}).returningLastInsertedId()
.executeInsert();
The executed query is:
insert into custom_company (name, is_big)
values ($1, case when $2 then 'Y' else 'N' end)
returning id
The parameters are: [ 'My Big Company', true ]
The result type is:
const insertCustomCompany: Promise<number>
Or a select:
const selectAllBigCompanies = connection.selectFrom(tCustomCompany)
.where(tCustomCompany.isBig)
.select({
id: tCustomCompany.id,
name: tCustomCompany.name,
isBig: tCustomCompany.isBig
}).executeSelectMany();
The executed query is:
select id as id, name as name, (is_big = 'Y') as isBig
from custom_company
where (is_big = 'Y')
The parameters are: []
The result type is:
const selectAllBigCompanies: Promise<{
id: number;
name: string;
isBig: boolean;
}[]>
Synchronous query runners
Some query runners support to execute the queries synchronously if you provide a Promise implementation that supports it, like synchronous-promise.
The query runners that support execute queries synchronously if you specify a synchronous Promise implementation are:
- BetterSqlite3QueryRunner
- Sqlite3WasmOO1QueryRunner
- ConsoleLogNoopQueryRunner
- MockQueryRunner
- NoopQueryRunner
For example:
import { BetterSqlite3QueryRunner } from "ts-sql-query/queryRunners/BetterSqlite3QueryRunner";
import * as betterSqlite3 from "better-sqlite3";
import { SynchronousPromise } from "synchronous-promise";
const db = betterSqlite3('foobar.db', options);
async function main() {
const connection = new DBConnection(new BetterSqlite3QueryRunner(db, { promise: SynchronousPromise }));
// Do your queries here, surrounding it by the sync function. For example:
const selectCompanies = sync(connection.selectFrom(tCompany)
.where(tCustomCompany.isBig)
.select({
id: tCompany.id,
name: tCompany.name
}).executeSelectMany());
var result = sync(connection.insertInto...)
result = sync(connection.update...)
result = sync(connection.delete...)
}
In the case of synchronous-promise, you will need this utility function that transforms a promise in a synchronous output:
/**
* This function unwraps the synchronous promise in a synchronous way,
* returning the result.
*/
function sync<T>(promise: Promise<T>): T {
const UNSET = Symbol('unset');
let result: T | typeof UNSET = UNSET;
let error: unknown | typeof UNSET = UNSET;
promise.then(
(r) => (result = r),
(e) => (error = e),
);
// Propagate error, if available
if (error !== UNSET) {
throw error;
}
// Propagate result, if available
if (result !== UNSET) {
return result;
}
// Note: This wrapper is to be used in combination with the `SynchronousPromise` type,
// which is not strictly Promise-spec-compliant because it does not defer when calling
// `.then`. See https://www.npmjs.com/package/synchronous-promise for more details.
// To ensure that we're indeed using a synchronous promise, ensure that the promise resolved
// immediately.
throw new Error(
'You performed a real async operation, not a database operation, ' +
'inside the function dedicated to calling the database',
);
}
Encrypted ID
Sometimes you want to encrypt the ID handled by the database. To do it, you can create a custom data type and define the type conversion using a type adapter or extending the default type adapter. During the type conversion, you can encrypt and decrypt with the strategy you like; for the next example, IDEncrypter will be used (included in ts-sql-query).
You can create the connection and define the rules to handle a type called encryptedID
:
import { PostgreSqlConnection } from "ts-sql-query/connections/PostgreSqlConnection";
import { IDEncrypter } from "ts-sql-query/extras/IDEncrypter";
class DBConnection extends PostgreSqlConnection<'DBConnection'> {
// PasswordEncrypter requires two strings of 16 chars of [A-Za-z0-9] working as passwords for the encrypt process
private encrypter = new IDEncrypter('3zTvzr3p67VC61jm', '60iP0h6vJoEaJo8c');
protected transformValueFromDB(value: unknown, type: string): unknown {
if (type === 'encryptedID') {
const id = super.transformValueFromDB(value, 'bigint');
if (typeof id === 'bigint') {
return this.encrypter.encrypt(id);
} else {
// return the value as is, it could be null
return id;
}
}
return super.transformValueFromDB(value, type);
}
protected transformValueToDB(value: unknown, type: string): unknown {
if (type === 'encryptedID') {
if (value === null || value === undefined) {
// In case of null or undefined send null to the database
return null;
} else if (typeof value === 'string') {
const id = this.encrypter.decrypt(value);
return super.transformValueToDB(id, 'bigint');
} else {
throw new Error('Invalid id: ' + value);
}
}
return super.transformValueToDB(value, type);
}
}
You can create the table, specifying the id type as custom
or customComparable
with type name encryptedID
and data type string
(the type of the encrypted data):
import { Table } from "ts-sql-query/Table";
const tCompany = new class TCompany extends Table<DBConnection, 'TCompany'> {
id = this.autogeneratedPrimaryKey<string>('id', 'customComparable', 'encryptedID');
name = this.column('name', 'string');
constructor() {
super('company'); // table name in the database
}
}();
const tCustomer = new class TCustomer extends Table<DBConnection, 'TCustomer'> {
id = this.autogeneratedPrimaryKey<string>('id', 'customComparable', 'encryptedID');
firstName = this.column('first_name', 'string');
lastName = this.column('last_name', 'string');
birthday = this.optionalColumn('birthday', 'localDate');
companyId = this.column<string>('company_id', 'customComparable', 'encryptedID');
constructor() {
super('customer'); // table name in the database
}
}();
If you execute an insert that returns the id, the id will be encrypted:
const id = await connection
.insertInto(tCompany)
.values({ name: 'ACME' })
.returningLastInsertedId()
.executeInsert()
The returned id will be uftSdCUhUTBQ0111
for id 1 in the database.
You can perform a select using the encrypted id:
let company = await connection
.selectFrom(tCompany)
.where(tCompany.id.equals('uftSdCUhUTBQ0111'))
.select({
id: tCompany.id,
name: tCompany.name
})
.executeSelectOne()
The id used in the query will be sent to the database decrypted.
See IDEncrypter for more information to know how the password is encrypted.
Globally Encrypted ID
Sometimes you want to encrypt the ID handled by the database and ensure is globally unique. To do it, you can create a custom data type and define the type conversion using a type adapter or extending the default type adapter. During the type conversion, you can encrypt and decrypt with the strategy you like; for the next example, IDEncrypter will be used (included in ts-sql-query). You will need to define a prefix per table. In the example the custom data type name follows the pattern ID:prefix
You can create the connection and define the rules to handle the ID type:
import { PostgreSqlConnection } from "ts-sql-query/connections/PostgreSqlConnection";
import { IDEncrypter } from "ts-sql-query/extras/IDEncrypter";
class DBConnection extends PostgreSqlConnection<'DBConnection'> {
// PasswordEncrypter requires two strings of 16 chars of [A-Za-z0-9] working as passwords for the encrypt process
private encrypter = new IDEncrypter('3zTvzr3p67VC61jm', '60iP0h6vJoEaJo8c');
protected transformValueFromDB(value: unknown, type: string): unknown {
if (type.startsWith('ID:')) {
const id = super.transformValueFromDB(value, 'bigint');
if (typeof id === 'bigint') {
const prefix = encrypted.substring(3) // 'ID:'.length
return this.encrypter.encrypt(id, prefix);
} else {
// return the value as is, it could be null
return id;
}
}
return super.transformValueFromDB(value, type);
}
protected transformValueToDB(value: unknown, type: string): unknown {
if (type.startsWith('ID:')) {
if (value === null || value === undefined) {
// In case of null or undefined send null to the database
return null;
} else if (typeof value === 'string') {
const prefix = encrypted.substring(3) // 'ID:'.length
const id = this.encrypter.decrypt(value, prefix);
return super.transformValueToDB(id, 'bigint');
} else {
throw new Error('Invalid id: ' + value);
}
}
return super.transformValueToDB(value, type);
}
}
You can create the table, specifying the id type as custom
or customComparable
with type name that starts with ID:
followed by a prefix and data type string
(the type of the encrypted data):
import { Table } from "ts-sql-query/Table";
const tCompany = new class TCompany extends Table<DBConnection, 'TCompany'> {
id = this.autogeneratedPrimaryKey<string>('id', 'customComparable', 'ID:co');
name = this.column('name', 'string');
constructor() {
super('company'); // table name in the database
}
}();
const tCustomer = new class TCustomer extends Table<DBConnection, 'TCustomer'> {
id = this.autogeneratedPrimaryKey<string>('id', 'customComparable', 'ID:cu');
firstName = this.column('first_name', 'string');
lastName = this.column('last_name', 'string');
birthday = this.optionalColumn('birthday', 'localDate');
companyId = this.column<string>('company_id', 'customComparable', 'ID:co'); // Be careful, use the proper prefix
constructor() {
super('customer'); // table name in the database
}
}();
If you execute an insert that returns the id, the id will be encrypted:
const id = await connection
.insertInto(tCompany)
.values({ name: 'ACME' })
.returningLastInsertedId()
.executeInsert()
The returned id will be coFJL3xTJZvP6Kd30d
for id 1 in the database.
You can perform a select using the encrypted id:
let company = await connection
.selectFrom(tCompany)
.where(tCompany.id.equals('coFJL3xTJZvP6Kd30d'))
.select({
id: tCompany.id,
name: tCompany.name
})
.executeSelectOne()
The id used in the query will be sent to the database decrypted.
See IDEncrypter for more information to know how the password is encrypted.
Extract columns from an object
Extract columns
Sometimes could be useful to extract all columns available in an object, like a table or view; this allows to use in a select, ensuring the select uses all columns defined in the provided object. For this purpose you can find the function extractColumnsFrom
in the file ts-sql-query/extras/utils
. This function receives the object that contains the columns as its first argument and optionally, as its second argument, an array with the name of the properties to exclude.
import { extractColumnsFrom } from "ts-sql-query/extras/utils";
const selectAll = connection.selectFrom(tCustomer)
.select(extractColumnsFrom(tCustomer))
.where(tCustomer.id.equals(9))
.executeSelectOne();
The executed query is:
select id as id, first_name as firstName, last_name as lastName, birthday as birthday, company_id as companyId, first_name || $1 || last_name as name, calculateAge(birthday) as age
from customer
where id = $2
The parameters are: [ " ", 9 ]
The result type is:
const selectAll: Promise<{
companyId: number;
id: number;
name: string;
firstName: string;
lastName: string;
birthday?: Date;
age?: number;
}
Additionally, if you want to get an array with the column names, you can call the function extractColumnNamesFrom
in the file ts-sql-query/extras/utils
.
import { extractColumnNamesFrom } from "ts-sql-query/extras/utils";
const tCustomerColumnNames = extractColumnNamesFrom(tCustomer);
Extract writable columns
Sometimes could be useful to extract all columns available in an object, like a table or view, excluding the one that cannot be use in an insert or update. This function is analogous to extractColumnsFrom
but ignoring computed ad virtual columns. For this purpose you can find the function extractWritableColumnsFrom
in the file ts-sql-query/extras/utils
. This function receives the object that contains the columns as its first argument and optionally, as its second argument, an array with the name of the properties to exclude.
import { extractWritableColumnsFrom } from "ts-sql-query/extras/utils";
const selectAll = connection.selectFrom(tCustomer)
.select(extractWritableColumnsFrom(tCustomer))
.where(tCustomer.id.equals(9))
.executeSelectOne();
The executed query is:
select id as id, first_name as firstName, last_name as lastName, birthday as birthday, company_id as companyId
from customer
where id = $1
The parameters are: [ 9 ]
The result type is:
const selectAll: Promise<{
id: number;
firstName: string;
lastName: string;
companyId: number;
birthday?: Date;
}>
Additionally, if you want to get an array with the writable column names, you can call the function extractWritableColumnNamesFrom
in the file ts-sql-query/extras/utils
.
import { extractWritableColumnNamesFrom } from "ts-sql-query/extras/utils";
const tCustomerWritableColumnNames = extractWritableColumnNamesFrom(tCustomer);
Extract writable shape
This function returns the insert or update shape where the property name in the value object and the remaped property are the same, like {property: 'property'}
. This function receives the object that contains the columns as its first argument and optionally, as its second argument, an array with the name of the properties to exclude.
import { extractWritableShapeFrom } from "ts-sql-query/extras/utils";
const tCustomerWritableShape = extractWritableShapeFrom(tCustomer);
Extract id columns
Sometimes could be useful to extract all primary key columns available in an object, like a table or view. This function is analogous to extractColumnsFrom
but only returning primary key columns. For this purpose you can find the function extractIdColumnsFrom
in the file ts-sql-query/extras/utils
. This function receives the object that contains the columns as its first argument and optionally, as its second argument, an array with the name of the properties to exclude.
import { extractIdColumnsFrom } from "ts-sql-query/extras/utils";
const idColumns = extractIdColumnsFrom(tCustomer);
Additionally, if you want to get an array with the writable column names, you can call the function extractIdColumnNamesFrom
in the file ts-sql-query/extras/utils
.
import { extractIdColumnNamesFrom } from "ts-sql-query/extras/utils";
const tCustomerIdColumnNames = extractIdColumnNamesFrom(tCustomer);
Extract autogenerated id columns
Sometimes could be useful to extract all primary key columns marked as autogenerated available in an object, like a table or view. This function is analogous to extractColumnsFrom
but only returning autogenerated primary key columns. For this purpose you can find the function extractAutogeneratedIdColumnsFrom
in the file ts-sql-query/extras/utils
. This function receives the object that contains the columns as its first argument and optionally, as its second argument, an array with the name of the properties to exclude.
import { extractAutogeneratedIdColumnsFrom } from "ts-sql-query/extras/utils";
const autogeneratedIdColumns = extractAutogeneratedIdColumnsFrom(tCustomer);
Additionally, if you want to get an array with the writable column names, you can call the function extractAutogeneratedIdColumnNamesFrom
in the file ts-sql-query/extras/utils
.
import { extractAutogeneratedIdColumnNamesFrom } from "ts-sql-query/extras/utils";
const tCustomerAutogeneratedIdColumnNames = extractAutogeneratedIdColumnNamesFrom(tCustomer);
Extract provided id columns
Sometimes could be useful to extract all primary key columns not marked as autogenerated (the value must be provided) available in an object, like a table or view. This function is analogous to extractColumnsFrom
but only returning non-autogenerated primary key columns. For this purpose you can find the function extractProvidedIdColumnsFrom
in the file ts-sql-query/extras/utils
. This function receives the object that contains the columns as its first argument and optionally, as its second argument, an array with the name of the properties to exclude.
import { extractProvidedIdColumnsFrom } from "ts-sql-query/extras/utils";
const providedIdColumns = extractProvidedIdColumnsFrom(tCustomer);
Additionally, if you want to get an array with the writable column names, you can call the function extractProvidedIdColumnNamesFrom
in the file ts-sql-query/extras/utils
.
import { extractProvidedIdColumnNamesFrom } from "ts-sql-query/extras/utils";
const tCustomerProvidedIdColumnNames = extractProvidedIdColumnNamesFrom(tCustomer);
Merge types
DEPRECATED: Use dynamicBooleanExpressionUsing instead
Note: Before use merge types verify:
- First: If you can use
IfValue
functions described at Easy dynamic queries instead. - Second: If you can use
dynamicBooleanExpresionUsing
function described at Complex dynamic boolean expressions instead.
Sometimes, when you write advanced dynamic queries, you end in a situation when you have a variable with type a union of several types of value source, and you want to use it in a query. ts-sql-query doesn't handle the union of value source types due to the complexity of inferring the proper resulting type. However, the function mergeType
(defined in the ts-sql-query/extras/utils
file) can merge the types allowing you to use the value source in other parts of the query.
import { mergeType } from "ts-sql-query/extras/utils";
const firstNameContains = 'ohn';
const lastNameContains = null;
const birthdayIs = null;
const searchOrderBy = 'name insensitive, birthday asc nulls last';
const hideId = false
let searchedCustomersWhere
if (firstNameContains) {
searchedCustomersWhere = tCustomer.firstName.contains(firstNameContains)
} else {
searchedCustomersWhere = connection.noValueBoolean()
}
if (lastNameContains) {
searchedCustomersWhere = mergeType(searchedCustomersWhere).or(tCustomer.lastName.contains(lastNameContains))
}
if (birthdayIs) {
searchedCustomersWhere = mergeType(searchedCustomersWhere).and(tCustomer.birthday.equals(birthdayIs))
}
searchedCustomersWhere = mergeType(searchedCustomersWhere)
let idColumn
if (hideId) {
idColumn = connection.optionalConst(null, 'int')
} else {
idColumn = tCustomer.id
}
idColumn = mergeType(idColumn)
const searchedCustomers = connection.selectFrom(tCustomer)
.where(searchedCustomersWhere)
.select({
id: idColumn,
name: tCustomer.firstName.concat(' ').concat(tCustomer.lastName),
birthday: tCustomer.birthday
})
.orderByFromString(searchOrderBy)
.executeSelectMany()
The executed query is:
select id as id, first_name || $1 || last_name as name, birthday as birthday
from customer
where first_name like ('%' || $2 || '%')
order by lower(name), birthday asc nulls last
The parameters are: [ ' ', 'ohn' ]
The result type is:
const searchedCustomers: Promise<{
id?: number;
name: string;
birthday?: Date;
}[]>
Utility types
Column keys
This type return the key name (properties in the object) of the columns contained a an object like a table or view definition.
import { ColumnKeys } from 'ts-sql-query/extras/types';
// Alias to type tCustomerColumns = "id" | "firstName" | "lastName" | "companyId" | "birthday" | "name" | "age"
type tCustomerColumns = ColumnKeys<typeof tCustomer>;
Writable column keys
This type return the key name (properties in the object) of the columns contained a an object like a table or view definition that can be use in an insert or update. This type is analogous to ColumnKeys
but ignoring computed ad virtual columns.
import { WritableColumnKeys } from 'ts-sql-query/extras/types';
// Alias to type tCustomerWritableColumns = "id" | "firstName" | "lastName" | "companyId" | "birthday"
type tCustomerWritableColumns = WritableColumnKeys<typeof tCustomer>;
Writable shape
This type returns the insert or update shape where the property name in the value object and the remaped property are the same, like {property: 'property'}
.
import { WritableShapeFor } from 'ts-sql-query/extras/types';
type WritableCompanyShape = WritableShapeFor<typeof tCompany>;
Id column keys
This type return the key name (properties in the object) of the id / primary keys columns contained a an object like a table or view definition. This type is analogous to ColumnKeys
but returning primary key columns.
import { IdColumnKeys } from 'ts-sql-query/extras/types';
// Alias to type tCustomerColumns = "id"
type tCustomerIdColumns = IdColumnKeys<typeof tCustomer>;
Autogenerated id column keys
This type return the key name (properties in the object) of the id / primary keys columns marked as autogenerated contained a an object like a table or view definition. This type is analogous to ColumnKeys
but returning autogenerated primary key columns.
import { AutogeneratedIdColumnKeys } from 'ts-sql-query/extras/types';
// Alias to type tCustomerAutogeneratedIdColumns = "id"
type tCustomerAutogeneratedIdColumns = AutogeneratedIdColumnKeys<typeof tCustomer>;
Provided id column keys
This type return the key name (properties in the object) of the id / primary keys columns not marked as autogenerated (the value must be provided) contained a an object like a table or view definition. This type is analogous to ColumnKeys
but returning non-autogenerated primary key columns.
import { ProvidedIdColumnKeys } from 'ts-sql-query/extras/types';
// In case tCustomer were not using an autogenerated primary key this will be an alias to type tCustomerAutogeneratedIdColumns = "id"
type tCustomerProvidedIdColumns = ProvidedIdColumnKeys<typeof tCustomer>;
Insertable values
This type returns the object type with the values that can be used in an insert for the provided table.
import { InsertableValues } from 'ts-sql-query/extras/types';
type InsertableCompany = InsertableValues<typeof tCompany>;
If you use a shape in your query you must use:
import { InsertableValuesShapedAs } from 'ts-sql-query/extras/types';
const myCustomerShape = {
newFirstName: 'firstName',
newLastName: 'lastName'
}
type InsertableShapedCompany = InsertableValuesShapedAs<typeof tCustomer, typeof myCustomerShape>;
Insertable row
This type returns the same type expected by the set clause in an insert.
Note: The difference with InsertableValues
is this type admits valid SQL objects that you can use in the insert sentence, where InsertableValues
do not.
import { InsertableRow } from 'ts-sql-query/extras/types';
type InserableCompanyRow = InsertableRow<typeof tCompany>;
If you use a shape in your query you must use:
import { InsertableRowShapedAs } from 'ts-sql-query/extras/types';
const myCustomerShape = {
newFirstName: 'firstName',
newLastName: 'lastName'
}
type InserableShapedCompanyRow = InsertableRowShapedAs<typeof tCustomer, typeof myCustomerShape>;
Updatable values
This type returns the object type with the values that can be used in an update for the provided table.
import { UpdatableValues } from 'ts-sql-query/extras/types';
type UpdatableCompany = UpdatableValues<typeof tCompany>;
If you use a shape in your query you must use:
import { UpdatableValuesShapedAs } from 'ts-sql-query/extras/types';
const myCustomerShape = {
newFirstName: 'firstName',
newLastName: 'lastName'
}
type UpdatableShapedCustomer = UpdatableValuesShapedAs<typeof tCustomer, typeof myCustomerShape>;
Updatable row
This type returns the same type expected by the set clause in an update.
Note: The difference with UpdatableValues
is this type admits valid SQL objects that you can use in the update sentence, where UpdatableValues
do not.
import { UpdatableRow } from 'ts-sql-query/extras/types';
type UpdatableCompanyRow = UpdatableRow<typeof tCompany>;
If you use a shape in your query you must use:
import { UpdatableRowShapedAs } from 'ts-sql-query/extras/types';
const myCustomerShape = {
newFirstName: 'firstName',
newLastName: 'lastName'
}
type UpdatableShapedCustomerRow = UpdatableRowShapedAs<typeof tCustomer, typeof myCustomerShape>;
Updatable values in case of conflict on insert
This type returns the object type with the values that can be used in an insert's onConflictDoUpdateSet
for the provided table.
import { UpdatableOnInsertConflictValues } from 'ts-sql-query/extras/types';
type UpdatableCompanyOnInsertConflict = UpdatableOnInsertConflictValues<typeof tCompany>;
If you use a shape in your query you must use:
import { UpdatableOnInsertConflictValuesShapedAs } from 'ts-sql-query/extras/types';
const myCustomerShape = {
newFirstName: 'firstName',
newLastName: 'lastName'
}
type UpdatableShapedCompanyOnInsertConflict = UpdatableOnInsertConflictValuesShapedAs<typeof tCustomer, typeof myCustomerShape>;
Updatable row in case of conflict on insert
This type returns the same type expected by the onConflictDoUpdateSet
clause in an insert.
Note: The difference with UpdatableOnInsertConflictValues
is this type admits valid SQL objects that you can use in the onConflictDoUpdateSet
sentence, where UpdatableOnInsertConflictValues
do not.
import { UpdatableOnInsertConflictRow } from 'ts-sql-query/extras/types';
type UpdatableCompanyOnInsertConflictRow = UpdatableOnInsertConflictRow<typeof tCompany>;
If you use a shape in your query you must use:
import { UpdatableOnInsertConflictRowShapedAs } from 'ts-sql-query/extras/types';
const myCustomerShape = {
newFirstName: 'firstName',
newLastName: 'lastName'
}
type UpdatableShapedCompanyOnInsertConflictRow = UpdatableOnInsertConflictRowShapedAs<typeof tCustomer, typeof myCustomerShape>;
Selected values
This type returns the result type of a select on a table or view where all its columns are included.
import { SelectedValues } from 'ts-sql-query/extras/types';
type Company = SelectedValues<typeof tCompany>;
Note: If you query project optional values in objects as always-required properties, use SelectedValuesProjectedAsNullable
instead.
Selected row
This type returns the result type of a select on a table or view where all its columns are included.
Note: This is just an alias to SelectedValues
for coherence and compatibility reasons.
import { SelectedRow } from 'ts-sql-query/extras/types';
type CompanyRow = SelectedRow<typeof tCompany>;
Note: If you query project optional values in objects as always-required properties, use SelectedRowProjectedAsNullable
instead.
Passing tables and views as parameter
If you want to pass a table or view to a function as a parameter, you must provide its type. For this purpose ts-sql-query offers you the types:
TableOrViewOf
: for use with regular tables or views that allows creating a reference to the table or view.TableOrViewLeftJoinOf
: for the case, the table or view is marked for use in a left join.
These types receive as first generic argument the type of the referenced table or view, and optionally as second argument the alias in case it has one. These types are the base type of the generic argument the function receives that will represent the real type.
To access the columns, you will need to transform the reference into a real instance of the table or view. To do it, you will need to call the fromRef
function and provide, as the first argument, the table or view represented by the reference (or the class of it) and, as the second argument, the referenced object.
import { fromRef, TableOrViewLeftJoinOf, TableOrViewOf } from 'ts-sql-query/extras/types';
function buildNumberOfCustomersSubquery<COMPANY extends TableOrViewOf<typeof tCompany, 'company'>>(connection: DBConnection, companyRef: COMPANY) {
const company = fromRef(tCompany, companyRef);
return connection
.subSelectUsing(company)
.from(tCustomer)
.where(tCustomer.companyId.equals(company.id))
.selectOneColumn(connection.countAll())
.forUseAsInlineQueryValue()
.valueWhenNull(0);
}
async function getCompanyInfoWithNumberOfCustomers(connection: DBConnection, id: number) {
const company = tCompany.as('company');
return await connection.selectFrom(company)
.select({
id: company.id,
name: company.name,
numberOfCustomers: buildNumberOfCustomersSubquery(connection, company)
})
.where(company.id.equals(id))
.executeSelectOne()
}
Query execution metadata
When you are implementing your own query runner, there is some metadata available about the query that is trying to be executed.
Query execution stack
The getQueryExecutionStack
will return a string with the stack trace where the query was requested to be executed and return undefined
if the information is unavailable.
import { getQueryExecutionStack } from 'ts-sql-query/queryRunners/QueryRunner';
import { InterceptorQueryRunner, QueryType } from "ts-sql-query/queryRunners/InterceptorQueryRunner";
class DurationLogginQueryRunner extends InterceptorQueryRunner<void> {
onQuery(queryType: QueryType, query: string, params: any[]): DurationPlayload {
const stack: string | undefined = getQueryExecutionStack(query, params)
console.log('query execution stack', stack)
}
onQueryResult(queryType: QueryType, query: string, params: any[], result: any, playload: void): void {
...
}
onQueryError(queryType: QueryType, query: string, params: any[], error: any, playload: void): void {
...
}
}
The stack strace will looks like:
Error: Query executed at
at CompoundSelectQueryBuilder.executeSelectMany (ts-sql-query/src/queryBuilders/SelectQueryBuilder.ts:187:24)
at myFunction (myFolder/myFile.ts:112:10)
at myOuterFunction (myFolder/myFile.ts:147:23)
at ...
Function executing the query
The getFunctionExecutingQuery
will return an object with information related to the function that requests to execute the query and return undefined
if the information is unavailable. Each property can be undefined
if that information is unavailable.
import { getFunctionExecutingQuery, FunctionExecutingQueryInformation } from 'ts-sql-query/queryRunners/QueryRunner';
import { InterceptorQueryRunner, QueryType } from "ts-sql-query/queryRunners/InterceptorQueryRunner";
class DurationLogginQueryRunner extends InterceptorQueryRunner<void> {
onQuery(queryType: QueryType, query: string, params: any[]): DurationPlayload {
const info : FunctionExecutingQueryInformation | undefined = getFunctionExecutingQuery(query, params)
if (!info) {
return
}
const functionName: string | undefined = info.functionName
const fileName: string | undefined = info.fileName
const lineNumber: string | undefined = info.lineNumber
const positionNumber: string | undefined = info.positionNumber
console.log('Name of the function where the query was requested to be executed', functionName)
console.log('Name of the file where the query was requested to be executed', fileName)
console.log('Line number where the query was requested to be executed', lineNumber)
console.log('Position in the line where the query was requested to be executed', positionNumber)
}
onQueryResult(queryType: QueryType, query: string, params: any[], result: any, playload: void): void {
...
}
onQueryError(queryType: QueryType, query: string, params: any[], error: any, playload: void): void {
...
}
}
Properties of the returned object:
- functionName: string with the function name that requested the execution of the query,
undefined
if that information is unavailable. - fileName: string with the file name (including path) that requested the execution of the query,
undefined
if that information is unavailable. - lineNumber: string with the line number in the file that requested the execution of the query,
undefined
if that information is unavailable. - positionNumber: string with the position in the line that requested the execution of the query,
undefined
if that information is unavailable.
Is select page count query
The Select page is the only place where ts-sql-query executes a second query to return the count of elements. The function isSelectPageCountQuery
allows you to identify if the requested query corresponds to the select count in a select page.
import { isSelectPageCountQuery } from 'ts-sql-query/queryRunners/QueryRunner';
import { InterceptorQueryRunner, QueryType } from "ts-sql-query/queryRunners/InterceptorQueryRunner";
class DurationLogginQueryRunner extends InterceptorQueryRunner<void> {
onQuery(queryType: QueryType, query: string, params: any[]): DurationPlayload {
const isCount: boolean = isSelectPageCountQuery(query, params)
console.log('the query is a select count in a select page', isCount)
}
onQueryResult(queryType: QueryType, query: string, params: any[], result: any, playload: void): void {
...
}
onQueryError(queryType: QueryType, query: string, params: any[], error: any, playload: void): void {
...
}
}
Query execution name
Before you execute a query, you can customize the query; in the customization possible, you can specify a property called queryExecutionName
to put an informative name for that query execution. You can use the getQueryExecutionName
to get that name (or undefined
if it was not provided).
import { getQueryExecutionName } from 'ts-sql-query/queryRunners/QueryRunner';
import { InterceptorQueryRunner, QueryType } from "ts-sql-query/queryRunners/InterceptorQueryRunner";
class DurationLogginQueryRunner extends InterceptorQueryRunner<void> {
onQuery(queryType: QueryType, query: string, params: any[]): DurationPlayload {
const name: string | undefined = getQueryExecutionName(query, params)
console.log('query execution name', name)
}
onQueryResult(queryType: QueryType, query: string, params: any[], result: any, playload: void): void {
...
}
onQueryError(queryType: QueryType, query: string, params: any[], error: any, playload: void): void {
...
}
}
You can specify the informative name in this way:
const customizedSelect = connection.selectFrom(tCustomer)
.where(tCustomer.id.equals(10))
.select({
id: tCustomer.id,
firstName: tCustomer.firstName,
lastName: tCustomer.lastName,
birthday: tCustomer.birthday
}).customizeQuery({
queryExecutionName: 'My query name'
})
.executeSelectOne()
Query execution metadata
Before you execute a query, you can customize the query; in the customization possible, you can specify a property called queryExecutionMetadata
to put additional metadata for that query execution. You can use the getQueryExecutionMetadata
to get that metadata (or undefined
if it was not provided).
import { getQueryExecutionMetadata } from 'ts-sql-query/queryRunners/QueryRunner';
import { InterceptorQueryRunner, QueryType } from "ts-sql-query/queryRunners/InterceptorQueryRunner";
class DurationLogginQueryRunner extends InterceptorQueryRunner<void> {
onQuery(queryType: QueryType, query: string, params: any[]): DurationPlayload {
const metadata: unkown = getQueryExecutionMetadata(query, params)
console.log('query execution metadata', metadata)
}
onQueryResult(queryType: QueryType, query: string, params: any[], result: any, playload: void): void {
...
}
onQueryError(queryType: QueryType, query: string, params: any[], error: any, playload: void): void {
...
}
}
You can specify the metadata in this way:
const customizedSelect = connection.selectFrom(tCustomer)
.where(tCustomer.id.equals(10))
.select({
id: tCustomer.id,
firstName: tCustomer.firstName,
lastName: tCustomer.lastName,
birthday: tCustomer.birthday
}).customizeQuery({
queryExecutionMetadata: { myMetadataProp: 'my metadata value' }
})
.executeSelectOne()