Decorators

Decorators for Abstracting Database Access and Object Transformations

Decorators are a powerful tool in TypeScript that allow for the abstraction of database access and transformation of objects into database records and vice versa. They provide a way to describe metadata that can be used to configure interaction with the database without directly modifying the code.

1. Table Decorator

Description: The Table decorator is used to define the database table associated with the class. This decorator takes parameters name and options, where name specifies the table name, and options allows specifying additional options.

Code:

import 'reflect-metadata';
import { TableDecoratorInterface } from '@decorators/index';
import { constants } from '@core/constants';

export function Table({ name, options }: TableDecoratorInterface) {
    return function(constructor: Function) {
        if (!name) {
            console.info(
                'Ви вказали назву таблиці не коректно або вона відсутня, тому назва буде взята за назвою класу'
            );
            name = constructor.name;
        }

        Reflect.defineMetadata(constants.decoratorsMetadata.table, { name, options }, constructor.prototype);
    };
}

Usage: This decorator can be used to define how a class maps to a database table. For example:

@Table({ name: 'users', options: { schema: 'public' } })
class User {
    @Column({ name: 'id', options: { type: 'integer', primary: true } })
    id: number;

    @Column({ name: 'username', options: { type: 'varchar', length: 255 } })
    username: string;
}

In this example, the Table decorator indicates that the User class corresponds to the users table in the public schema. All properties of the class will represent columns of the table.

  1. The work of a decorator Column

Consider the Column decorator, which is medium in size and illustrates the process of working with metadata in TypeScript.

Decorator Column

Code:

import 'reflect-metadata';
import { ColumnDecoratorInterface, ColumnMetadataInterface, ColumnOptionsDecoratorInterface } from '@decorators/index';
import { constants } from '@core/constants';

export function Column(decoratorParams?: ColumnDecoratorInterface) {
    return function(target: any, propertyKey: string) {
        let name = propertyKey;
        let options: ColumnOptionsDecoratorInterface = { nullable: true };

        if (decoratorParams) {
            name = decoratorParams.name || propertyKey;
            options = { ...options, ...decoratorParams.options };
        }

        const columns: ColumnMetadataInterface[] = Reflect.getMetadata(constants.decoratorsMetadata.columns, target) || [];

        columns.forEach(column => {
            if (!column.options?.dataType) {
                throw Error('Ви не вказали тип колонки!');
            }
        });

        columns.push({ name, options, propertyKey });
        Reflect.defineMetadata(constants.decoratorsMetadata.columns, columns, target);
    };
}

How the Column Decorator Works

  1. Decorator Declaration The Column decorator takes an optional decoratorParams parameter that allows configuring the column's name and options. The decorator is used to attach metadata to a class property.

  2. Value Assignment let name = propertyKey;: The default column name is the property name of the class. let options: ColumnOptionsDecoratorInterface = { nullable: true };: By default, the column can be nullable. if (decoratorParams) { ... }: If decorator parameters are provided, then: name = decoratorParams.name || propertyKey;: The column name is set based on the decorator parameters if provided. options = { ...options, ...decoratorParams.options };: The column options are updated from the decorator parameters.

  3. Reading Metadata const columns: ColumnMetadataInterface[] = Reflect.getMetadata(constants.decoratorsMetadata.columns, target) || [];: Reads existing column metadata for the target class. If no metadata is present, an empty array is initialized.

  4. Column Type Check columns.forEach(column => { ... });: Checks if a data type is specified for all columns. if (!column.options?.dataType) { throw Error('Column type not specified!'); }: Throws an error if a data type is not specified.

  5. Adding a New Column columns.push({ name, options, propertyKey });: Adds a new column to the metadata array.

  6. Saving Updated Metadata Reflect.defineMetadata(constants.decoratorsMetadata.columns, columns, target);: Updates the column metadata for the target class, saving the new data.

Using Metadata from Decorators Metadata defined through decorators can be retrieved and used to create database schemas or configure queries. For example:

getPreparedModels(models: ClassInterface[]): TableIngotInterface<DT>[] {
    const preparedModels: TableIngotInterface<DT>[] = [];

    for (let model of models) {
        const table: TableInterface<DT>
            = Reflect.getMetadata(constants.decoratorsMetadata.table, model.prototype);
        const metadataColumns: ColumnMetadataInterface<DT>[]
            = Reflect.getMetadata(constants.decoratorsMetadata.columns, model.prototype);
        // Інші метадані...

        // Обробка та підготовка моделі для використання з базою даних
        preparedModels.push({
            tableName: table.name,
            columns: metadataColumns,
            // Інші властивості...
        });
    }

    return preparedModels;
}

Usage Example

Class Declaration

import { Column } from '@decorators/column';

class User {
    @Column({ name: 'user_id', options: { primary: true, type: 'integer' } })
    id: number;

    @Column({ name: 'user_name', options: { type: 'string', length: 50 } })
    name: string;
}

How the Code Works

  1. Decorator Initialization

    • For the id property, the Column decorator is applied with parameters { name: 'user_id', options: { primary: true, type: 'integer' } }. This indicates that the column will be named user_id, be a primary key, and have an integer type.

    • For the name property, the Column decorator is applied with parameters { name: 'user_name', options: { type: 'string', length: 50 } }. This indicates that the column will be named user_name, be of type string, and have a maximum length of 50 characters.

  2. Decorator Processing When applying decorators to the id and name properties, the Column decorator stores metadata about these columns in the User object using Reflect.defineMetadata. The decorator checks if types are specified for all columns and throws errors if types are not provided.

  3. Metadata Storage As a result, metadata about the columns for the User class is stored in the class metadata and can be used for generating SQL queries or other database operations.

In this example, the metadata obtained from the Table and Column decorators is used to form a table schema that can be used to interact with the database. This allows dynamic adaptation to changes in the data structure without needing to modify the code interacting with the database.

Last updated