Base SQLite DataSource
Introduction
Section titled “Introduction”BaseSqliteSqlDataSource is the base class provided by Xeno.JS for implementing data sources that work with SQLite-compatible databases through Drizzle ORM and the libSQL client.
The class does not implement CRUD operations and does not define methods such as findById() or save().
Instead, it provides protected access to the database:
protected get db(): LibSQLDatabase<TSchema>A concrete data source extends the class and uses this.db to execute Drizzle queries.
Application │ ├── AppBuilder.addDb() │ │ │ ├── enableSqlLite = true │ └── @libsql/client + Drizzle │ ├── TOKENS.DB_CONTEXT │ └── Custom DataSource │ └── BaseSqliteSqlDataSource │ └── this.dbThe public class is exported by @xeno-js/core as:
BaseSqliteSqlDataSourcePrerequisites
Section titled “Prerequisites”To use the SQLite/libSQL database path, the application needs:
@xeno-js/core;drizzle-orm;@libsql/client.
The Xeno.JS package declares drizzle-orm and @libsql/client as optional peer dependencies, so applications using this database integration must install the packages they require.
npm install @xeno-js/core drizzle-orm @libsql/clientXeno.JS currently uses the @libsql/client client together with the Drizzle drizzle-orm/libsql integration.
Define the database schema
Section titled “Define the database schema”Define the tables using the Drizzle SQLite schema API and collect them into an application schema.
For example:
import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core'
export const users = sqliteTable('users', { id: integer('id').primaryKey(), email: text('email').notNull(),})
export const dbSchema = { users,}
export type ProjectDbSchema = typeof dbSchemaThe schema can then be used as the generic parameter of BaseSqliteSqlDataSource.
Using the schema type allows the database exposed through this.db to retain the application’s Drizzle schema typing.
Create the SQLite DataSource
Section titled “Create the SQLite DataSource”A concrete data source extends BaseSqliteSqlDataSource.
import { eq } from 'drizzle-orm'import type { BaseSqliteSqlDataSource } from '@xeno-js/core/db'
import { users } from '../db/schema'import type { ProjectDbSchema } from '../db/schema'
export class UserDataSource extends BaseSqliteSqlDataSource<ProjectDbSchema> { public async findById(id: number) { const [user] = await this.db .select() .from(users) .where(eq(users.id, id))
return user }}The important part is:
this.dbdb is protected, so it is available to the concrete data source but is not exposed as part of the public API of the resolved data source.
The data source does not need to create a LibSQLDatabase manually.
Constructor
Section titled “Constructor”The constructor of the base class receives a DbContext<TSchema>:
export abstract class BaseSqliteSqlDataSource< TSchema extends Dictionary = Dictionary,> { constructor(private readonly _db: DbContext<TSchema>) {}
protected get db(): LibSQLDatabase<TSchema> { return this._db as LibSQLDatabase<TSchema> }}DbContext<TSchema> is Xeno.JS’s database context type. It is a union that supports both:
NodePgDatabase<TSchema>for PostgreSQL;LibSQLDatabase<TSchema>for SQLite/libSQL.
For BaseSqliteSqlDataSource, the protected getter narrows the context to:
LibSQLDatabase<TSchema>A concrete data source therefore receives TOKENS.DB_CONTEXT from the dependency injection container.
new UserDataSource(container.resolve(TOKENS.DB_CONTEXT))Configure SQLite
Section titled “Configure SQLite”SQLite/libSQL is configured through AppBuilder.addDb().
The important configuration option is:
enableSqlLite: trueFor example:
const builder = new AppBuilder<AppRegistry>()
builder.addDb((options, config) => { options.connectionString = config.getOrThrow('DATABASE_URL') options.enableSqlLite = true})addDb() initializes the database configuration with:
{ connectionString: '', enableSqlLite: false,}Therefore, SQLite/libSQL must be explicitly enabled.
When enableSqlLite is true, Xeno.JS uses its SQLite/libSQL database path instead of the PostgreSQL path.
The resulting flow is:
AppBuilder.addDb() │ ├── enableSqlLite = true │ ▼DbModule │ ▼DbUtils.addSqlLite() │ ▼DbSqlLiteClientFactory │ ├── @libsql/client └── drizzle-orm/libsql │ ▼DbContextConfigure the connection string
Section titled “Configure the connection string”The SQLite/libSQL factory creates the client with the configured connection string:
const client = createClient({ url: opts.connectionString,})The client is then passed to Drizzle:
return drizzle({ client })Therefore the value assigned to connectionString is passed directly to @libsql/client as its url.
For example:
builder.addDb((options, config) => { options.connectionString = config.getOrThrow('DATABASE_URL') options.enableSqlLite = true})The exact URL format depends on the libSQL client configuration being used by the application.
Register the DataSource
Section titled “Register the DataSource”After configuring the database, register the concrete data source through addServices().
import { AppBuilder, TOKENS } from '@xeno-js/core'
import type { AppRegistry } from './infrastructure/xeno-registry/app-registry'import { UserDataSource } from './infrastructure/datasources/user.datasource'
const builder = new AppBuilder<AppRegistry>()
builder .addDb((options, config) => { options.connectionString = config.getOrThrow('DATABASE_URL') options.enableSqlLite = true }) .addServices((services) => { services.addScoped('USER_DATA_SOURCE', (container) => { return new UserDataSource( container.resolve(TOKENS.DB_CONTEXT), ) }) })USER_DATA_SOURCE is an application-specific token. It is not a built-in Xeno.JS token.
The token should therefore be added to the application’s registry.
Type the Application Registry
Section titled “Type the Application Registry”Use XenoDbRegistry with the application’s Drizzle schema.
import type { XenoDbRegistry } from '@xeno-js/core/db'
import type { ProjectDbSchema } from '../db/schema'import type { UserDataSource } from '../datasources/user.datasource'
export interface AppRegistry extends XenoDbRegistry<ProjectDbSchema> { USER_DATA_SOURCE: UserDataSource}The database schema is supplied as the generic parameter:
XenoDbRegistry<ProjectDbSchema>The registry can then contain application-specific dependency injection tokens such as:
USER_DATA_SOURCEBuild and resolve
Section titled “Build and resolve”Complete the application bootstrap with build():
const container = await builder.build()Because the data source is registered as scoped, resolve it from a scope:
const scope = container.createScope()
const dataSource = scope.resolve('USER_DATA_SOURCE')
const user = await dataSource.findById(42)
await scope.dispose()The scope is important because Xeno.JS registers DB_CONTEXT as a scoped service.
The relationship is:
Scope │ ├── TRANSACTION_STATE │ ├── DB_CONTEXT │ └── USER_DATA_SOURCEComplete example
Section titled “Complete example”A minimal application structure can be:
src/├── bootstrap.ts├── domain/│ └── ...└── infrastructure/ ├── db/ │ └── schema.ts ├── datasources/ │ └── user.datasource.ts └── xeno-registry/ └── app-registry.tsDatabase schema
Section titled “Database schema”import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core'
export const users = sqliteTable('users', { id: integer('id').primaryKey(), email: text('email').notNull(),})
export const dbSchema = { users,}
export type ProjectDbSchema = typeof dbSchemaApplication registry
Section titled “Application registry”import type { XenoDbRegistry } from '@xeno-js/core/db'
import type { ProjectDbSchema } from '../db/schema'import type { UserDataSource } from '../datasources/user.datasource'
export interface AppRegistry extends XenoDbRegistry<ProjectDbSchema> { USER_DATA_SOURCE: UserDataSource}Data source
Section titled “Data source”import { eq } from 'drizzle-orm'import { BaseSqliteSqlDataSource } from '@xeno-js/core/db'
import { users } from '../db/schema'import type { ProjectDbSchema } from '../db/schema'
export class UserDataSource extends BaseSqliteSqlDataSource<ProjectDbSchema> { public async findById(id: number) { const [user] = await this.db .select() .from(users) .where(eq(users.id, id))
return user }}Bootstrap
Section titled “Bootstrap”import { AppBuilder, TOKENS } from '@xeno-js/core'
import type { AppRegistry } from './infrastructure/xeno-registry/app-registry'import { UserDataSource } from './infrastructure/datasources/user.datasource'
export async function bootstrap() { const builder = new AppBuilder<AppRegistry>()
builder .addDb((options, config) => { options.connectionString = config.getOrThrow('DATABASE_URL') options.enableSqlLite = true }) .addServices((services) => { services.addScoped('USER_DATA_SOURCE', (container) => { return new UserDataSource( container.resolve(TOKENS.DB_CONTEXT), ) }) })
return builder.build()}const container = await bootstrap()
const scope = container.createScope()
const dataSource = scope.resolve('USER_DATA_SOURCE')
const user = await dataSource.findById(42)
await scope.dispose()Use the database directly
Section titled “Use the database directly”BaseSqliteSqlDataSource is useful when a data source needs to execute SQLite/libSQL-specific queries through Drizzle.
For example:
export class UserDataSource extends BaseSqliteSqlDataSource<ProjectDbSchema> { public async findAll() { return this.db .select() .from(users) }}The base class does not impose a particular application-level API on the data source.
A concrete data source can therefore contain:
- specific queries;
- filters;
- joins supported by the configured Drizzle dialect;
- aggregations;
- insert operations;
- update operations;
- delete operations;
- queries used by Repository or ReadDao.
The responsibility for implementing those queries remains with the concrete data source.
BaseSqliteSqlDataSource vs BasePostgresSqlDataSource
Section titled “BaseSqliteSqlDataSource vs BasePostgresSqlDataSource”Xeno.JS exposes separate base data source classes for PostgreSQL and SQLite/libSQL.
For PostgreSQL:
BasePostgresSqlDataSource ↓NodePgDatabase<TSchema>For SQLite/libSQL:
BaseSqliteSqlDataSource ↓LibSQLDatabase<TSchema>The corresponding database clients are also different.
The SQLite/libSQL path uses:
@libsql/client ↓drizzle-orm/libsql ↓LibSQLDatabaseThe PostgreSQL path uses the PostgreSQL Drizzle integration instead.
Choose the base data source that corresponds to the database path configured through addDb().
SQLite/libSQL only
Section titled “SQLite/libSQL only”BaseSqliteSqlDataSource is intended for the SQLite/libSQL path.
The database must therefore be configured with:
options.enableSqlLite = trueFor example:
builder.addDb((options, config) => { options.connectionString = config.getOrThrow('DATABASE_URL') options.enableSqlLite = true})If enableSqlLite remains false, Xeno.JS uses the PostgreSQL database factory instead.
The two paths are selected inside DbModule:
const db = opts.enableSqlLite ? await DbUtils.addSqlLite(opts) : await DbUtils.addDbClient(opts)This means the setting determines which implementation of DbContext is created.
Transaction context
Section titled “Transaction context”DB_CONTEXT is registered by DbModule as a scoped service.
Xeno.JS also registers TRANSACTION_STATE as scoped and creates DB_CONTEXT using that transaction state.
When there is an active transaction, the DB_CONTEXT proxy delegates database operations to the active transaction state.
Conceptually:
Scope │ ├── TRANSACTION_STATE │ │ │ └── active transaction │ └── DB_CONTEXT │ ├── normal database │ └── active transaction when presentA data source that receives DB_CONTEXT should therefore be registered as scoped when it captures that scoped dependency:
services.addScoped('USER_DATA_SOURCE', (container) => { return new UserDataSource( container.resolve(TOKENS.DB_CONTEXT), )})This allows the data source to receive the DB_CONTEXT belonging to the current scope.
Common problems
Section titled “Common problems”this.db is not available
Section titled “this.db is not available”db is protected.
It cannot be accessed through the resolved data source:
const dataSource = scope.resolve('USER_DATA_SOURCE')
dataSource.db // not accessibleIt is available inside the concrete data source:
class UserDataSource extends BaseSqliteSqlDataSource<ProjectDbSchema> { public query() { return this.db.select().from(users) }}SQLite is not being used
Section titled “SQLite is not being used”Check that addDb() explicitly enables the SQLite/libSQL path:
builder.addDb((options, config) => { options.connectionString = config.getOrThrow('DATABASE_URL') options.enableSqlLite = true})If enableSqlLite is left as false, Xeno.JS selects the PostgreSQL database factory.
The libSQL client cannot be loaded
Section titled “The libSQL client cannot be loaded”Verify that the application has installed:
npm install @libsql/client@libsql/client is an optional peer dependency of @xeno-js/core.
Drizzle SQLite types are not available
Section titled “Drizzle SQLite types are not available”Verify that drizzle-orm is installed:
npm install drizzle-ormThe SQLite data source imports:
import type { LibSQLDatabase } from 'drizzle-orm/libsql'and the Xeno.JS SQLite database factory uses:
import { drizzle } from 'drizzle-orm/libsql'DB_CONTEXT cannot be resolved
Section titled “DB_CONTEXT cannot be resolved”Make sure the database module has been configured:
builder.addDb(...)before:
await builder.build()DB_CONTEXT is registered by DbModule, which is queued by addDb().
The data source is resolved from the root container
Section titled “The data source is resolved from the root container”If the data source is registered with:
services.addScoped(...)resolve it through a scope:
const scope = container.createScope()
const dataSource = scope.resolve('USER_DATA_SOURCE')Then dispose of the scope when it is no longer needed:
await scope.dispose()Source naming note
Section titled “Source naming note”The public class is:
BaseSqliteSqlDataSourceand it is exported from @xeno-js/core.
[Unverified] The current repository source file is named:
base-sqllite.datasource.tsThe filename contains sqllite, while the public class name uses Sqlite:
BaseSqliteSqlDataSourceFor application documentation and imports from @xeno-js/core, use the public class name rather than relying on the internal source filename.
Checklist
Section titled “Checklist”To implement a SQLite/libSQL DataSource:
- install
@xeno-js/core; - install
drizzle-orm; - install
@libsql/client; - define the Drizzle SQLite tables;
- create the application database schema type;
- specialize
XenoDbRegistry<ProjectDbSchema>; - create a class extending
BaseSqliteSqlDataSource<ProjectDbSchema>; - use
this.dbfor Drizzle queries; - configure
addDb(); - set
options.enableSqlLite = true; - provide the database connection string;
- register the data source with
addServices(); - use
addScoped()when the data source depends on the scopedDB_CONTEXT; - resolve the data source from a scope;
- dispose of the scope when finished.
Related Docs
Section titled “Related Docs”- Data Overview
- Node PostgreSQL
- SQLite
- Unit of Work
- Service Registration
- Service Resolution
- Dependency Graph
Support Us
Section titled “Support Us”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
