Skip to content

ID manipulation

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 = type.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 = type.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.

Note

The custom type name must start with ID: for the conversion logic to recognize and apply the encryption/decryption rules correctly.

See IDEncrypter for more information to know how the password is encrypted.