Data sources in Zibri are used to connect eg. to databases.
They provide a lot of functionality out of the box, including:
A data source needs too implement DataSourceInterface. Zibri also provides more specific, predefined classes, like the PostgresDataSource that you can extend from instead.
The data source then needs to be decorated with @DataSource.
The example below illustrates how a data source might be setup:
// src/data-sources/db/db.data-source.ts
import { PostgresDataSource, PostgresOptions, BaseEntity, DataSource, Newable } from 'zibri';
import { Test } from '../../models';
@DataSource()
export class DbDataSource extends PostgresDataSource {
options: PostgresOptions = {
host: 'localhost',
port: 5432,
username: 'postgres',
password: 'password',
database: 'db',
synchronize: true
};
entities: Newable<BaseEntity>[] = [Test];
}
As you can see, the configuration of a data source is pretty straightforward. We also added our first entity to the data source, Test:
// src/models/test.model.ts
import { BaseEntity, Entity, Property } from 'zibri';
@Entity()
export class Test extends BaseEntity {
@Property.string()
value!: string;
}
An entity to be included in a data source needs to extend BaseEntity, which defines an id property.
In order for the data source to map the properties, you need to decorate them with the @Property decorator. This is also used for validation.
To access data from the data source, you use repositories for specific entities. They can simply be injected without needing you to define them:
// src/services/test.service.ts
import { InjectRepository, Repository } from 'zibri';
import { Test } from '../models';
export class TestService {
constructor(
@InjectRepository(Test) // The last 2 generics are not required
private readonly testRepository: Repository<Test, TestCreateData, TestUpdateData>,
) {}
}
A repository always exposes the methods:
The full, detailed definition can be found under the repository typedoc.
In most cases you probably don't want to get all entities from a data source, but a filtered selection. For that all retrieving methods have an optional where filter property:
// returns all test entities where value is exactly '42'
await this.testRepository.findAll({ where: { value: '42' } });
// returns all test entities where value ends with '42'
await this.testRepository.findAll({ where: { value: { iLike: '%42' } } });
// returns all test entities where value ends with '42' AND is not '42'
await this.testRepository.findAll({ where: { value: { iLike: '%42', not: '42' } } });
// returns all test entities where value either:
// - ends with '42' AND is not '42'
// - OR is '43'
await this.testRepository.findAll({ where: { value: [{ iLike: '%42', not: '42' }, '43'] } });
A transaction can be started from a data source:
// src/services/test.service.ts
import { InjectRepository, Repository } from 'zibri';
import { Test } from '../models';
export class TestService {
constructor(
@InjectRepository(Test)
private readonly testRepository: Repository<Test, TestCreateData, TestUpdateData>,
private readonly dataSource: DbDataSource
) {}
async doSomething(): Promise<void> {
const transaction: Transaction = await this.dataSource.startTransaction();
try {
const test1: Test = await this.testRepository.create({ value: '42' }, { transaction });
const test2: Test = await this.testRepository.create({ value: '43' }, { transaction });
await transaction.commit();
}
catch (error) {
await transaction.rollback();
throw error;
}
}
}
Migrations need to be provided on the data source:
// src/data-sources/db/db.data-source.ts
import { PostgresDataSource, PostgresOptions, BaseEntity, DataSource, Newable } from 'zibri';
import { Test } from '../../models';
import { RenameValuePropertyMigration } from './migrations';
@DataSource()
export class DbDataSource extends PostgresDataSource {
options: PostgresOptions = {
host: 'localhost',
port: 5432,
username: 'postgres',
password: 'password',
database: 'db',
synchronize: true
};
entities: Newable<BaseEntity>[] = [Test];
migrations: Newable<Migration>[] = [RenameValuePropertyMigration];
}
Let's take a look at what the RenameValuePropertyMigration does.
For this we assume that the entity Test had a property before that was called oldValue, which has been renamend to the current property value. The migration handles that rename:
// src/data-sources/db/migrations/rename-value-property.migration.ts
import { Injectable, Migration, Transaction, Version } from 'zibri';
import { Test } from '../../../models';
import { DbDataSource } from '../db.data-source.ts';
@Injectable()
class RenameValuePropertyMigration extends Migration {
version: Version = '0.0.1';
constructor() {
super(DbDataSource);
}
override async up(transaction: Transaction): Promise<void> {
await this.dataSource.changePropertyOfEntity(Test, 'oldValue', { type: 'string', name: 'value' }, transaction);
}
// eslint-disable-next-line typescript/require-await
override async down(): Promise<void> {
throw new Error('Not implemented yet.');
}
}
As you can see, each migration needs to have a version for which it should run. That version is compared against the global one provided to zibri, which is the package.json version by default.
The ChangeSetRepository is an extended version of a Repository, that automatically tracks who changed what & when.
It is automatically chosen with no further configuration on your end when the entity provided to @InjectRepository has a property called changeSets which is an array.
If you want to remove a property from tracking, you can set the excludeFromChangeSets flag:
// src/models/test.model.ts
import { BaseEntity, Entity, Property } from 'zibri';
@Entity()
export class Test extends BaseEntity {
@Property.string({ excludeFromChangeSets: true })
value!: string;
}
The SoftDeleteRepository is an extended version of a ChangeSetRepository, which adds soft delete functionality.