Skip to content

Data Overview

Xeno.JS provides the application-facing infrastructure you need to access relational data and execute database operations without making the database provider the center of your application architecture.

The workflow is:

Choose a database
↓
Configure it with AppBuilder
↓
Build the application
↓
Resolve the database services
↓
Use a provider-specific Data Source
↓
Use Unit of Work when operations must run in a transaction

Xeno.JS currently supports:

  • PostgreSQL through the Node PostgreSQL integration.
  • SQLite through the libSQL client.

For provider-specific configuration and usage, see:

You need:

  • a Xeno.JS application;
  • the database provider dependencies required by your selected database;
  • a database connection string;
  • an AppBuilder instance.

The database is configured during application bootstrap. Application code should consume the registered database services rather than creating database clients directly.

Use AppBuilder.addDb() to configure the database.

For PostgreSQL, keep enableSqlLite disabled:

import { AppBuilder } from '@xeno-js/core'
const app = new AppBuilder()
app.addDb((options, config) => {
options.connectionString =
config.get('DB_CONNECTION_STRING') ??
'postgres://user:password@localhost:5432/myapp'
options.enableSqlLite = false
})
await app.build()

For SQLite, enable SQLite:

import { AppBuilder } from '@xeno-js/core'
const app = new AppBuilder()
app.addDb((options) => {
options.connectionString = 'file:./data/app.db'
options.enableSqlLite = true
})
await app.build()

The important configuration options are:

Option Purpose
connectionString Connection URL passed to the configured database provider.
enableSqlLite Selects SQLite when set to true.

When enableSqlLite is false, Xeno.JS uses the PostgreSQL database client.

Database registration belongs in the application bootstrap.

A typical application setup looks like:

import { AppBuilder } from '@xeno-js/core'
const app = new AppBuilder()
app
.addDb((options, config) => {
options.connectionString =
config.get('DB_CONNECTION_STRING') ??
'postgres://user:password@localhost:5432/myapp'
options.enableSqlLite = false
})
.addPipeline()
const container = await app.build()

addDb() queues the database module during bootstrap. build() initializes the configured modules and returns the application service container.

After the build completes, the database services are available through dependency injection.

The database context is registered under the DB_CONTEXT token.

You can resolve it directly from AppBuilder:

const db = app.resolve('DB_CONTEXT')

Or from the built service container:

const container = await app.build()
const db = container.resolve('DB_CONTEXT')

The exact type of DB_CONTEXT depends on the application’s Xeno registry.

For most application code, however, you should not pass the database context throughout the application manually. Instead, create a provider-specific Data Source and inject it where data access is required.

Xeno.JS exposes provider-specific base Data Sources for application infrastructure.

For PostgreSQL, extend:

BasePostgresSqlDataSource

For SQLite, extend:

BaseSqliteSqlDataSource

These base classes give your Data Source access to the configured database through the protected db property.

import { BasePostgresSqlDataSource } from '@xeno-js/core/db'
export class UserDataSource extends BasePostgresSqlDataSource {
async findUserById(id: string) {
return this.db.query.users.findFirst({
where: (users, { eq }) => eq(users.id, id),
})
}
}
import { BaseSqliteSqlDataSource } from '@xeno-js/core/db'
export class UserDataSource extends BaseSqliteSqlDataSource {
async findUserById(id: string) {
return this.db.query.users.findFirst({
where: (users, { eq }) => eq(users.id, id),
})
}
}

The provider-specific pages contain the complete configuration and Data Source examples:

Your application Data Source is a normal dependency-injection service.

For example:

const app = new AppBuilder()
app.addDb((options, config) => {
options.connectionString =
config.get('DB_CONNECTION_STRING') ??
'postgres://user:password@localhost:5432/myapp'
options.enableSqlLite = false
})
app.addServices((services) => {
services.addScoped('USER_DATA_SOURCE', (scope) => {
return new UserDataSource(scope.resolve('DB_CONTEXT'))
})
})
const container = await app.build()

The Data Source can then be resolved from the application container or injected into another service.

For more information about registration and lifetimes, see:

When an operation requires multiple database changes to succeed as one unit, use the registered Unit of Work.

Resolve it through the UNIT_OF_WORK token:

const unitOfWork = app.resolve('UNIT_OF_WORK')

Then execute the transactional operation with runInTransaction():

await unitOfWork.runInTransaction(async () => {
await userDataSource.createUser(user)
await userDataSource.createProfile(profile)
}, undefined)

The transaction API is callback-based.

You do not manually call:

begin()
commit()
rollback()

Instead, the work performed inside runInTransaction() is executed as one transaction.

For the complete transaction workflow, see Unit of Work.

A typical application that uses Xeno.JS Data infrastructure can be organized like this:

HTTP / transport
↓
Command or Query
↓
Handler
↓
Application service / Repository
↓
Data Source
↓
Database

For transactional operations:

Command Handler
↓
Unit of Work
↓
Data Source(s)
↓
Database transaction

The database provider remains infrastructure. Your application services, handlers, and repositories consume the data access services through explicit dependencies.

The following example shows the basic composition of a PostgreSQL application.

import { AppBuilder } from '@xeno-js/core'
import { BasePostgresSqlDataSource } from '@xeno-js/core/db'
class UserDataSource extends BasePostgresSqlDataSource {
async findUserById(id: string) {
return this.db.query.users.findFirst({
where: (users, { eq }) => eq(users.id, id),
})
}
}
const app = new AppBuilder()
app.addDb((options, config) => {
options.connectionString =
config.get('DB_CONNECTION_STRING') ??
'postgres://user:password@localhost:5432/myapp'
options.enableSqlLite = false
})
app.addServices((services) => {
services.addScoped('USER_DATA_SOURCE', (scope) => {
return new UserDataSource(scope.resolve('DB_CONTEXT'))
})
})
await app.build()
const dataSource = app.resolve('USER_DATA_SOURCE')
const user = await dataSource.findUserById('user-123')

This example demonstrates the complete Data workflow:

  1. configure the database;
  2. register the database with addDb();
  3. register an application Data Source;
  4. build the application;
  5. resolve the Data Source;
  6. perform data access.

The provider-specific pages show the complete implementation for each database.

Go to Node PostgreSQL.

Use this page when you need to:

  • configure a PostgreSQL connection;
  • register PostgreSQL in AppBuilder;
  • create a PostgreSQL Data Source;
  • access the PostgreSQL database from application infrastructure.

Go to SQLite.

Use this page when you need to:

  • configure SQLite;
  • register SQLite in AppBuilder;
  • create a SQLite Data Source;
  • access the SQLite database from application infrastructure.

Go to Unit of Work.

Use this page when multiple database operations must execute as one transactional operation.

Make sure the database module has been registered:

app.addDb((options) => {
options.connectionString = 'postgres://user:password@localhost:5432/myapp'
})

Then build the application before resolving the service:

await app.build()
const db = app.resolve('DB_CONTEXT')

The application fails while bootstrapping the database

Section titled “The application fails while bootstrapping the database”

Check:

  1. the connectionString;
  2. the selected provider;
  3. the provider dependency installation;
  4. whether the configured database is reachable.

The error may be wrapped by AppBuilder as a bootstrap failure for the database module.

Check enableSqlLite.

PostgreSQL:

options.enableSqlLite = false

SQLite:

options.enableSqlLite = true

Make sure addDb() has been configured and the application has been built.

UNIT_OF_WORK is registered by the database module.

app.addDb((options) => {
options.connectionString = 'postgres://user:password@localhost:5432/myapp'
})
await app.build()
const unitOfWork = app.resolve('UNIT_OF_WORK')

Before implementing database access, verify:

  • I selected PostgreSQL or SQLite.
  • The required provider dependency is installed.
  • I configured connectionString.
  • I configured enableSqlLite correctly.
  • I registered the database with AppBuilder.addDb().
  • I called build() before resolving database services.
  • I use DB_CONTEXT through dependency injection.
  • My application Data Source extends the correct provider-specific base class.
  • I register my Data Source with the appropriate lifetime.
  • I use UNIT_OF_WORK for transactional operations.
  • I keep database infrastructure outside the domain layer.

Xeno.JS is an MIT-licensed open source project. It can grow thanks to the support of these awesome people. If you’d like to join them, please read more at support section