@venizia/ignis-docs 0.2.1-0 → 0.2.1-1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/content/best-practices/architectural-patterns.md +3 -3
- package/content/best-practices/code-style-standards/naming-conventions.md +1 -1
- package/content/best-practices/contribution-workflow.md +2 -2
- package/content/best-practices/error-handling.md +94 -89
- package/content/best-practices/security-guidelines.md +5 -5
- package/content/extensions/components/api-reference.md +22 -21
- package/content/extensions/components/authentication/api.md +64 -28
- package/content/extensions/components/authentication/errors.md +19 -5
- package/content/extensions/components/authentication/index.md +19 -18
- package/content/extensions/components/authentication/usage.md +55 -30
- package/content/extensions/components/authorization/api.md +351 -84
- package/content/extensions/components/authorization/errors.md +51 -17
- package/content/extensions/components/authorization/getting-started.md +227 -0
- package/content/extensions/components/authorization/index.md +24 -16
- package/content/extensions/components/authorization/usage.md +45 -21
- package/content/extensions/components/health-check.md +15 -9
- package/content/extensions/components/index.md +24 -90
- package/content/extensions/components/mail/api.md +105 -54
- package/content/extensions/components/mail/errors.md +12 -10
- package/content/extensions/components/mail/index.md +32 -13
- package/content/extensions/components/mail/usage.md +26 -18
- package/content/extensions/components/request-tracker.md +18 -14
- package/content/extensions/components/socket-io/api.md +377 -882
- package/content/extensions/components/socket-io/errors.md +49 -51
- package/content/extensions/components/socket-io/index.md +72 -88
- package/content/extensions/components/socket-io/usage.md +107 -117
- package/content/extensions/components/static-asset/api.md +83 -31
- package/content/extensions/components/static-asset/errors.md +18 -7
- package/content/extensions/components/static-asset/index.md +18 -11
- package/content/extensions/components/static-asset/usage.md +11 -6
- package/content/extensions/components/template/index.md +3 -3
- package/content/extensions/components/websocket/api.md +58 -27
- package/content/extensions/components/websocket/errors.md +3 -3
- package/content/extensions/components/websocket/index.md +8 -7
- package/content/extensions/components/websocket/usage.md +21 -8
- package/content/extensions/helpers/cron/index.md +8 -7
- package/content/extensions/helpers/crypto/index.md +16 -8
- package/content/extensions/helpers/crypto/reference.md +96 -24
- package/content/extensions/helpers/env/index.md +14 -10
- package/content/extensions/helpers/error/index.md +99 -23
- package/content/extensions/helpers/index.md +61 -47
- package/content/extensions/helpers/inversion/index.md +23 -6
- package/content/extensions/helpers/inversion/reference.md +30 -22
- package/content/extensions/helpers/kafka/admin.md +3 -2
- package/content/extensions/helpers/kafka/compile-binary.md +69 -44
- package/content/extensions/helpers/kafka/consumer.md +24 -21
- package/content/extensions/helpers/kafka/examples.md +22 -234
- package/content/extensions/helpers/kafka/index.md +30 -62
- package/content/extensions/helpers/kafka/producer.md +31 -26
- package/content/extensions/helpers/kafka/schema-registry.md +19 -14
- package/content/extensions/helpers/logger/hf-logger.md +49 -22
- package/content/extensions/helpers/logger/index.md +37 -12
- package/content/extensions/helpers/logger/pino.md +31 -11
- package/content/extensions/helpers/logger/reference.md +243 -52
- package/content/extensions/helpers/network/api.md +65 -30
- package/content/extensions/helpers/network/index.md +32 -11
- package/content/extensions/helpers/queue/index.md +17 -6
- package/content/extensions/helpers/queue/reference.md +52 -25
- package/content/extensions/helpers/redis/index.md +29 -11
- package/content/extensions/helpers/redis/reference.md +85 -28
- package/content/extensions/helpers/secrets/index.md +82 -12
- package/content/extensions/helpers/socket-io/api.md +40 -21
- package/content/extensions/helpers/socket-io/index.md +26 -10
- package/content/extensions/helpers/storage/api.md +42 -24
- package/content/extensions/helpers/storage/index.md +10 -7
- package/content/extensions/helpers/types/index.md +20 -7
- package/content/extensions/helpers/types/reference.md +53 -14
- package/content/extensions/helpers/uid/index.md +166 -10
- package/content/extensions/helpers/websocket/api.md +52 -13
- package/content/extensions/helpers/websocket/index.md +7 -4
- package/content/extensions/helpers/worker-thread/index.md +9 -5
- package/content/extensions/helpers/worker-thread/reference.md +20 -20
- package/content/extensions/index.md +38 -39
- package/content/extensions/src-details/mcp-server.md +96 -548
- package/content/guides/core-concepts/persistent/datasources.md +2 -2
- package/content/guides/core-concepts/persistent/index.md +5 -1
- package/content/guides/core-concepts/persistent/pglite.md +218 -0
- package/content/guides/core-concepts/persistent/postgres-drivers.md +21 -15
- package/content/guides/core-concepts/persistent/search-meilisearch.md +22 -12
- package/content/guides/core-concepts/persistent/search-typesense.md +26 -14
- package/content/guides/core-concepts/persistent/sqlite.md +296 -0
- package/content/guides/core-concepts/persistent/transactions.md +12 -11
- package/content/guides/get-started/5-minute-quickstart.md +59 -206
- package/content/guides/get-started/philosophy.md +135 -670
- package/content/guides/get-started/setup.md +53 -74
- package/content/guides/migrations/redis-helpers-migration.md +4 -3
- package/content/guides/migrations/scoped-rbac-migration.md +5 -5
- package/content/guides/migrations/unified-connectors-migration.md +7 -6
- package/content/guides/tutorials/realtime-chat.md +1 -1
- package/content/references/base/application.md +63 -21
- package/content/references/base/components.md +3 -3
- package/content/references/base/connectors.md +14 -14
- package/content/references/base/controllers.md +12 -12
- package/content/references/base/datasources-reference.md +32 -31
- package/content/references/base/datasources.md +6 -6
- package/content/references/base/dependency-injection.md +12 -11
- package/content/references/base/filter-system/application-usage.md +54 -30
- package/content/references/base/filter-system/array-operators.md +24 -46
- package/content/references/base/filter-system/comparison-operators.md +47 -67
- package/content/references/base/filter-system/default-filter.md +59 -53
- package/content/references/base/filter-system/fields-order-pagination.md +92 -146
- package/content/references/base/filter-system/index.md +25 -13
- package/content/references/base/filter-system/json-filtering.md +45 -184
- package/content/references/base/filter-system/list-operators.md +23 -53
- package/content/references/base/filter-system/logical-operators.md +63 -121
- package/content/references/base/filter-system/null-operators.md +34 -104
- package/content/references/base/filter-system/pattern-matching.md +40 -55
- package/content/references/base/filter-system/quick-reference.md +86 -198
- package/content/references/base/filter-system/range-operators.md +18 -46
- package/content/references/base/filter-system/tips.md +6 -6
- package/content/references/base/filter-system/use-cases.md +33 -15
- package/content/references/base/grpc-controllers.md +53 -17
- package/content/references/base/index.md +5 -3
- package/content/references/base/middlewares.md +11 -10
- package/content/references/base/models-reference.md +17 -17
- package/content/references/base/models.md +4 -3
- package/content/references/base/providers.md +8 -8
- package/content/references/base/repositories/advanced.md +224 -326
- package/content/references/base/repositories/index.md +19 -6
- package/content/references/base/repositories/mixins.md +5 -5
- package/content/references/base/repositories/relations.md +160 -293
- package/content/references/base/repositories/soft-deletable.md +16 -6
- package/content/references/base/secrets.md +17 -13
- package/content/references/base/services.md +6 -4
- package/content/references/configuration/environment-variables.md +31 -23
- package/content/references/configuration/index.md +6 -4
- package/content/references/index.md +1 -1
- package/content/references/utilities/duration.md +85 -0
- package/content/references/utilities/index.md +5 -1
- package/content/references/utilities/jsx-reference.md +11 -11
- package/content/references/utilities/jsx.md +2 -2
- package/content/references/utilities/module.md +78 -25
- package/content/references/utilities/request.md +2 -1
- package/content/references/utilities/retry.md +139 -0
- package/content/references/utilities/schema.md +2 -2
- package/content/references/utilities/statuses-reference.md +4 -4
- package/content/references/utilities/statuses.md +5 -5
- package/dist/mcp-server/index.js +0 -0
- package/dist/mcp-server/tools/github/list-project-files.tool.d.ts +1 -1
- package/dist/mcp-server/tools/github/list-project-files.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/github/list-project-files.tool.js +3 -3
- package/dist/mcp-server/tools/github/search-code.tool.js +1 -1
- package/dist/mcp-server/tools/github/verify-dependencies.tool.js +1 -1
- package/dist/mcp-server/tools/github/view-source-file.tool.js +1 -1
- package/package.json +17 -16
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
# SQLite
|
|
2
|
+
|
|
3
|
+
IGNIS ships a **SQLite connector** at `@venizia/ignis/sqlite`, driven by libsql. It is the second engine on the engine-neutral relational tier, so `@model`, `@repository`, `@datasource`, filters, transactions and soft delete all mean the same thing they mean on Postgres.
|
|
4
|
+
|
|
5
|
+
They do not all *do* the same thing. Read this table first - it is what a Postgres user needs before anything else.
|
|
6
|
+
|
|
7
|
+
| Capability | PostgreSQL | SQLite | What happens |
|
|
8
|
+
| :--- | :--- | :--- | :--- |
|
|
9
|
+
| `ilike` | native `ILIKE` | none | Maps onto `LIKE`, which already folds ASCII case |
|
|
10
|
+
| `like` | case-**sensitive** | case-**insensitive** | Silently matches more rows than on Postgres |
|
|
11
|
+
| `regexp`, `iregexp` | native | none | Throws `501` `core.not_supported` |
|
|
12
|
+
| `contains`, `containedBy`, `overlaps` | array operators | no array type | Throws `501` `core.not_supported` |
|
|
13
|
+
| `lock` (`SELECT ... FOR UPDATE`) | row locks | file locks only | Throws `501` `core.not_supported` |
|
|
14
|
+
| Isolation levels | three (`IsolationLevels`) | none | `isolationLevel` throws; pass `beginMode` instead |
|
|
15
|
+
| Storage classes | ~28 types | five | `jsonb` -> json-mode `text`, `bytea` -> `blob`, `boolean` -> 0/1 `integer` |
|
|
16
|
+
| JSON paths | `col #>> '{a,b}'` | `json_extract(col, '$."a"."b"')` | Same filter syntax, different SQL |
|
|
17
|
+
| Timestamps | `timestamptz` | no date type | ISO 8601 UTC strings in a `text` column |
|
|
18
|
+
| NULL sort order | NULL sorts **high** | NULL sorts **low** | `order: ['score ASC']` puts NULLs last on Postgres, first here |
|
|
19
|
+
|
|
20
|
+
Every refusal throws `501 Not Implemented` with `normalized.code: 'core.not_supported'`. None of them silently emits different SQL.
|
|
21
|
+
|
|
22
|
+
The two divergences that do not throw are the dangerous ones. `like` widens, and `nlike` narrows - on SQLite `{ nlike: 'alpha' }` **drops** the row holding `Alpha`. NULL ordering inverts. Both are pinned per engine by the conformance suite.
|
|
23
|
+
|
|
24
|
+
> [!IMPORTANT] Optional peer dependency
|
|
25
|
+
> `@libsql/client` is an optional peer, and the connector never reaches the `@venizia/ignis` root barrel. Import from the sub-path.
|
|
26
|
+
>
|
|
27
|
+
> ```bash
|
|
28
|
+
> bun add @libsql/client
|
|
29
|
+
> ```
|
|
30
|
+
|
|
31
|
+
## What does work
|
|
32
|
+
|
|
33
|
+
- **Every CRUD verb**, including `.returning()` on insert, update and delete. MySQL cannot do that, so nothing here is emulated with a read-then-write.
|
|
34
|
+
- **Real transactions**, with `commit()`, `rollback()` and the same transaction object your services already pass around.
|
|
35
|
+
- **The whole filter vocabulary** minus the four operators above - `and`/`or`, `between`, `inq`, `nin`, `gt`, `like`, null handling, nested logical groups.
|
|
36
|
+
- **JSON path filtering and JSON path updates**, through `json_extract` and `json_set`.
|
|
37
|
+
- **Relations, hidden properties, default filters, soft delete, `count`, `skip`/`limit`.**
|
|
38
|
+
- **One driver for four deployments**: `:memory:`, a local file, a remote Turso database and an embedded replica.
|
|
39
|
+
|
|
40
|
+
## Wiring the datasource
|
|
41
|
+
|
|
42
|
+
```typescript
|
|
43
|
+
import { createClient } from '@libsql/client';
|
|
44
|
+
import type { Client } from '@libsql/client';
|
|
45
|
+
import { datasource } from '@venizia/ignis';
|
|
46
|
+
import type { TAnyDataSourceSchema, ValueOrPromise } from '@venizia/ignis';
|
|
47
|
+
import { BaseSqliteDataSource } from '@venizia/ignis/sqlite';
|
|
48
|
+
import type { ISqliteDataSourceSettings } from '@venizia/ignis/sqlite';
|
|
49
|
+
import { LibSqlDriver } from '@venizia/ignis/sqlite/libsql';
|
|
50
|
+
|
|
51
|
+
@datasource({ driver: LibSqlDriver })
|
|
52
|
+
export class SqliteDataSource extends BaseSqliteDataSource<
|
|
53
|
+
ISqliteDataSourceSettings,
|
|
54
|
+
TAnyDataSourceSchema,
|
|
55
|
+
{},
|
|
56
|
+
Client
|
|
57
|
+
> {
|
|
58
|
+
constructor() {
|
|
59
|
+
super({
|
|
60
|
+
name: SqliteDataSource.name,
|
|
61
|
+
config: { url: process.env.APP_ENV_SQLITE_URL ?? 'file:./data.db' },
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
override configure(): ValueOrPromise<void> {
|
|
66
|
+
this.client = createClient(this.settings);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
There is no `getConnectionString()` override. SQLite inherits one that returns `settings.url` - the libsql URL **is** the connection string. Postgres leaves the method abstract because no framework code can guess a `postgresql://` URL.
|
|
72
|
+
|
|
73
|
+
| Setting | Type | Meaning |
|
|
74
|
+
| :--- | :--- | :--- |
|
|
75
|
+
| `url` | `string` | `:memory:`, `file:./data.db`, `libsql://<host>`, or an embedded-replica URL |
|
|
76
|
+
| `authToken` | `string` | Turso credential; omit for a local file |
|
|
77
|
+
|
|
78
|
+
## Models
|
|
79
|
+
|
|
80
|
+
Same decorators, `sqliteTable` instead of `pgTable`, and the enrichers come from `@venizia/ignis/sqlite`:
|
|
81
|
+
|
|
82
|
+
```typescript
|
|
83
|
+
import { model } from '@venizia/ignis';
|
|
84
|
+
import {
|
|
85
|
+
BaseSqliteEntity,
|
|
86
|
+
generateIdColumnDefs,
|
|
87
|
+
generateTzColumnDefs,
|
|
88
|
+
} from '@venizia/ignis/sqlite';
|
|
89
|
+
import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core';
|
|
90
|
+
|
|
91
|
+
@model({ type: 'entity' })
|
|
92
|
+
export class Product extends BaseSqliteEntity<typeof Product.schema> {
|
|
93
|
+
static override schema = sqliteTable('Product', {
|
|
94
|
+
...generateIdColumnDefs({ id: { dataType: 'string' } }),
|
|
95
|
+
...generateTzColumnDefs(),
|
|
96
|
+
name: text('name').notNull(),
|
|
97
|
+
isActive: integer('is_active', { mode: 'boolean' }).notNull().default(true),
|
|
98
|
+
metadata: text('metadata', { mode: 'json' }).$type<Record<string, unknown>>(),
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Three column choices differ from the Postgres twin:
|
|
104
|
+
|
|
105
|
+
- **JSON is a json-mode `text` column.** Declare the mode. A JSON path on a plain `text` column is rejected rather than compiled into a `json_extract` that quietly returns `NULL`.
|
|
106
|
+
- **Booleans are `integer(..., { mode: 'boolean' })`** - SQLite stores 0 and 1.
|
|
107
|
+
- **Timestamps are ISO 8601 strings.** `generateTzColumnDefs()` handles this, defaulting to `(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`. Comparisons stay lexicographic, so ordering by a timestamp still works.
|
|
108
|
+
|
|
109
|
+
`generateIdColumnDefs` accepts `dataType: 'string'` and `dataType: 'number'`. There is no `'big-number'` - an integer primary key in SQLite is already the 64-bit rowid.
|
|
110
|
+
|
|
111
|
+
## Repositories
|
|
112
|
+
|
|
113
|
+
`DefaultSqliteRepository` is the base most repositories want. `ReadableSqliteRepository`, `PersistableSqliteRepository` and `SoftDeletableSqliteRepository` sit beside it, mirroring the Postgres chain.
|
|
114
|
+
|
|
115
|
+
```typescript
|
|
116
|
+
import { repository } from '@venizia/ignis';
|
|
117
|
+
import { DefaultSqliteRepository } from '@venizia/ignis/sqlite';
|
|
118
|
+
|
|
119
|
+
@repository({ model: Product, dataSource: SqliteDataSource })
|
|
120
|
+
export class ProductRepository extends DefaultSqliteRepository<typeof Product.schema> {}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Reads look exactly like Postgres reads:
|
|
124
|
+
|
|
125
|
+
```typescript
|
|
126
|
+
await this.productRepository.find({
|
|
127
|
+
filter: {
|
|
128
|
+
where: { and: [{ isActive: true }, { name: { like: 'wid%' } }] },
|
|
129
|
+
order: ['createdAt DESC'],
|
|
130
|
+
limit: 20,
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Remember that `like` is case-insensitive here, so `{ like: 'wid%' }` also matches `Widget`. Non-ASCII is not folded, so `'ÉCOLE' LIKE 'é%'` is still false. Never set `PRAGMA case_sensitive_like=ON` - it would break `ilike` too.
|
|
136
|
+
|
|
137
|
+
JSON paths use the same dot notation as Postgres and compile to SQLite syntax:
|
|
138
|
+
|
|
139
|
+
```typescript
|
|
140
|
+
{ where: { 'metadata.tier': { eq: 'gold' } } }
|
|
141
|
+
// SQL: json_extract("metadata", '$."tier"') = ?
|
|
142
|
+
|
|
143
|
+
{ where: { 'metadata.score': { gt: 50 } } }
|
|
144
|
+
// SQL: json_extract("metadata", '$."score"') > ? -- no cast; json_extract is already typed
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Postgres needs a numeric cast because `#>>` always returns text. SQLite does not, because `json_extract` hands back a JSON number as `INTEGER` or `REAL`. See [JSON/JSONB Filtering](/references/base/filter-system/json-filtering) for the path grammar.
|
|
148
|
+
|
|
149
|
+
## Transactions
|
|
150
|
+
|
|
151
|
+
SQLite has no isolation levels. Every SQLite transaction is already serializable, so the axis is a **locking mode**:
|
|
152
|
+
|
|
153
|
+
| `beginMode` | Statement | Takes the write lock |
|
|
154
|
+
| :--- | :--- | :--- |
|
|
155
|
+
| `SqliteBeginModes.IMMEDIATE` (default) | `BEGIN IMMEDIATE` | At `BEGIN` |
|
|
156
|
+
| `SqliteBeginModes.DEFERRED` | `BEGIN DEFERRED` | At the first write |
|
|
157
|
+
| `SqliteBeginModes.EXCLUSIVE` | `BEGIN EXCLUSIVE` | At `BEGIN`, blocking readers too |
|
|
158
|
+
|
|
159
|
+
```typescript
|
|
160
|
+
import { SqliteBeginModes } from '@venizia/ignis/sqlite';
|
|
161
|
+
|
|
162
|
+
const transaction = await this.dataSource.beginTransaction({
|
|
163
|
+
beginMode: SqliteBeginModes.IMMEDIATE,
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
try {
|
|
167
|
+
await this.productRepository.create({ data: { name: 'widget' }, options: { transaction } });
|
|
168
|
+
await transaction.commit();
|
|
169
|
+
} catch (error) {
|
|
170
|
+
// Nested: a first rollback that itself fails throws, and would replace the original error.
|
|
171
|
+
try {
|
|
172
|
+
await transaction.rollback();
|
|
173
|
+
} catch (rollbackError) {
|
|
174
|
+
this.logger.error('Rollback failed | %s', rollbackError);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
throw error;
|
|
178
|
+
}
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
`commit()` and `rollback()` throw on failure, exactly as they do on Postgres. See [Transactions](./transactions) for the full contract.
|
|
182
|
+
|
|
183
|
+
The default is `IMMEDIATE`, not SQLite's own `DEFERRED`. A deferred transaction takes its write lock at the first write, and that upgrade fails outright with `SQLITE_BUSY` when another writer got there first. `IMMEDIATE` waits on the busy timeout instead.
|
|
184
|
+
|
|
185
|
+
Passing `isolationLevel` throws rather than being ignored. Ignoring it would leave you believing `SERIALIZABLE` was honoured.
|
|
186
|
+
|
|
187
|
+
A `beginMode` outside those three throws too. The mode is written straight into the `BEGIN` statement, which the driver runs verbatim, so it has to come from the closed set. Matching is exact - `'immediate'` is refused, because nothing upper-cases it for you.
|
|
188
|
+
|
|
189
|
+
### One connection, so writes serialise
|
|
190
|
+
|
|
191
|
+
`beginTransaction()` borrows the single libsql connection from a 1-slot pool. A second transaction waits. SQLite allows one writer at a time regardless, so this costs no throughput SQLite was going to give.
|
|
192
|
+
|
|
193
|
+
Reads are the part to watch. A query on the pooled connector while a transaction is open runs **inside** that transaction. Pass `options.transaction` when the work belongs to the transaction, and route work that must stay outside through the driver's `acquire()`.
|
|
194
|
+
|
|
195
|
+
> [!WARNING] Transactions need a local database
|
|
196
|
+
> `beginTransaction()` throws `501` when the libsql client is not a `file:` or `:memory:` one. A remote client opens a stream per statement and closes it, so `BEGIN` would neither hold nor error - the transaction would silently not exist. Use a local file or an embedded replica.
|
|
197
|
+
|
|
198
|
+
### The acquire timeout
|
|
199
|
+
|
|
200
|
+
A transaction that never commits holds the one slot forever. Without a bound, every later `beginTransaction()` in the process hangs silently. So the wait is bounded:
|
|
201
|
+
|
|
202
|
+
| Option | Type | Default | Meaning |
|
|
203
|
+
| :--- | :--- | :--- | :--- |
|
|
204
|
+
| `acquireTimeoutMs` | `number` | `LibSqlDriver.DEFAULT_ACQUIRE_TIMEOUT_MS` (30000) | Max ms to wait for the connection before rejecting |
|
|
205
|
+
| `maxWaitingClients` | `number` | unlimited | Max queued waiters; over the limit, `acquire()` rejects immediately |
|
|
206
|
+
| `scope` | `string` | `'LibSqlDriver'` | Logger scope |
|
|
207
|
+
| `client` | `Client` | required | The libsql client the driver borrows |
|
|
208
|
+
|
|
209
|
+
The timeout bounds the **wait for the connection**, never the transaction itself. It fires only under contention, so a migration or a bulk seed running alone never trips it. When it does fire you get a named error, not a dead process:
|
|
210
|
+
|
|
211
|
+
```
|
|
212
|
+
[LibSqlDriver][acquire] Could not borrow the single libsql connection
|
|
213
|
+
| [acquire] Acquire timed out after 30000ms.
|
|
214
|
+
| An unreleased transaction still holds it - commit/rollback it, or raise `acquireTimeoutMs`
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
To change any of these, construct the driver yourself with `useDriver()`. That assigns the driver and builds the connector in one step, and bypasses `@datasource({ driver })`:
|
|
218
|
+
|
|
219
|
+
```typescript
|
|
220
|
+
override configure(): void {
|
|
221
|
+
const client = createClient(this.settings);
|
|
222
|
+
|
|
223
|
+
this.useDriver({
|
|
224
|
+
driver: new LibSqlDriver({ client, acquireTimeoutMs: 120_000 }),
|
|
225
|
+
schema: this.getSchema(),
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
`size` is not an option. One slot **is** the mutual exclusion the single connection lacks, so it is pinned at 1.
|
|
231
|
+
|
|
232
|
+
## Schema and migrations
|
|
233
|
+
|
|
234
|
+
drizzle-kit, with the `sqlite` dialect:
|
|
235
|
+
|
|
236
|
+
```typescript
|
|
237
|
+
// drizzle.config.ts
|
|
238
|
+
import { defineConfig } from 'drizzle-kit';
|
|
239
|
+
|
|
240
|
+
export default defineConfig({
|
|
241
|
+
dialect: 'sqlite',
|
|
242
|
+
schema: './src/models/**/*.model.ts',
|
|
243
|
+
out: './migrations',
|
|
244
|
+
dbCredentials: { url: 'file:./data.db' },
|
|
245
|
+
});
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
Use `dialect: 'turso'` instead for a remote database - it adds `authToken` to `dbCredentials`.
|
|
249
|
+
|
|
250
|
+
```bash
|
|
251
|
+
bun run drizzle-kit generate
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
Apply them at boot, before the first repository call:
|
|
255
|
+
|
|
256
|
+
```typescript
|
|
257
|
+
import { drizzle } from 'drizzle-orm/libsql';
|
|
258
|
+
import { migrate } from 'drizzle-orm/libsql/migrator';
|
|
259
|
+
|
|
260
|
+
export const applyMigrations = async (opts: { dataSource: SqliteDataSource }): Promise<void> => {
|
|
261
|
+
const client = opts.dataSource.getClient();
|
|
262
|
+
|
|
263
|
+
await migrate(drizzle({ client }), { migrationsFolder: './migrations' });
|
|
264
|
+
};
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
## Out of scope in this release
|
|
268
|
+
|
|
269
|
+
Three components declare their models as `pgTable` and are Postgres-only for now. A SQLite datasource cannot back them:
|
|
270
|
+
|
|
271
|
+
| Component | Why |
|
|
272
|
+
| :--- | :--- |
|
|
273
|
+
| **Authentication** | `User`, `Role`, `Permission` and `PolicyDefinition` are `pgTable` models |
|
|
274
|
+
| **Authorization** | The Casbin adapters type their connector as Drizzle's `PgDatabase` |
|
|
275
|
+
| **StaticAssetComponent** | Its base model is a `pgTable` with `jsonb` and Postgres indexes |
|
|
276
|
+
|
|
277
|
+
Everything else - your own models, repositories, services, controllers, REST routes - works on either engine.
|
|
278
|
+
|
|
279
|
+
## Runnable example
|
|
280
|
+
|
|
281
|
+
`examples/sqlite-quickstart/` is this page as an app: a `sqliteTable` model with a `text` id and `text({ mode: 'json' })` metadata, `generateTzColumnDefs` for the timestamp default, and the same repository and controller the PGlite example uses.
|
|
282
|
+
|
|
283
|
+
```bash
|
|
284
|
+
cd examples/sqlite-quickstart
|
|
285
|
+
bun install
|
|
286
|
+
bun run start # http://localhost:3000/api/notes
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
## See Also
|
|
290
|
+
|
|
291
|
+
- [PGlite](./pglite) - the other embedded engine, with real Postgres semantics
|
|
292
|
+
- [Postgres Drivers & Supabase](./postgres-drivers) - the driver seam
|
|
293
|
+
- [DataSources](./datasources) - schema auto-discovery and `configure()`
|
|
294
|
+
- [Transactions](./transactions) - the transaction object and connection safety
|
|
295
|
+
- [JSON/JSONB Filtering](/references/base/filter-system/json-filtering) - the JSON path grammar
|
|
296
|
+
- [Connectors](/references/base/connectors) - the engine-neutral contract
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
IGNIS supports explicit transaction objects that can be passed across multiple services and repositories, allowing for complex, multi-step business logic to be atomic.
|
|
4
4
|
|
|
5
5
|
> [!NOTE] PostgreSQL-only capability
|
|
6
|
-
> Real transactions are a **PostgreSQL connector** capability - `BasePostgresDataSource.getCapabilities()` returns `{ transactions: true }` and its `beginTransaction()` opens a real database transaction, as documented below. The typesense connector inherits the engine-neutral `AbstractDataSource` default
|
|
6
|
+
> Real transactions are a **PostgreSQL connector** capability - `BasePostgresDataSource.getCapabilities()` returns `{ transactions: true }` and its `beginTransaction()` opens a real database transaction, as documented below. The typesense connector inherits the engine-neutral `AbstractDataSource` default. Calling `beginTransaction()` on it throws a `501 Not Implemented` (`normalized.code: 'core.not_supported'`) via the shared `throwNotSupported` utility. See [Connectors](/references/base/connectors) for the capabilities model.
|
|
7
7
|
|
|
8
8
|
## Using Transactions
|
|
9
9
|
|
|
@@ -51,18 +51,18 @@ try {
|
|
|
51
51
|
> `COMMIT` can genuinely fail - a deadlock, a serialization failure under `SERIALIZABLE`, a dropped
|
|
52
52
|
> connection, a deferred constraint firing at commit time. When it does, `commit()` throws rather
|
|
53
53
|
> than resolving, so you never report success on a write that was never persisted. The connection is
|
|
54
|
-
> then destroyed instead of being returned to the pool
|
|
54
|
+
> then destroyed instead of being returned to the pool. It may still hold an open
|
|
55
55
|
> transaction that the next borrower would inherit.
|
|
56
56
|
>
|
|
57
57
|
> `rollback()` behaves the same way when it is the FIRST verb to fail. One deliberate exception
|
|
58
|
-
> keeps the everyday `catch { await tx.rollback(); throw error; }` pattern safe
|
|
58
|
+
> keeps the everyday `catch { await tx.rollback(); throw error; }` pattern safe. Calling
|
|
59
59
|
> `rollback()` on a transaction that already ended BY FAILURE (a failed `COMMIT` or a failed prior
|
|
60
|
-
> `ROLLBACK`) is a silent no-op
|
|
60
|
+
> `ROLLBACK`) is a silent no-op. Nothing was committed and the connection is already destroyed, so
|
|
61
61
|
> the rollback's goal is achieved and your original error survives. The nested-try form above is
|
|
62
62
|
> still the safest general pattern, because a FIRST rollback that itself fails does throw.
|
|
63
63
|
>
|
|
64
|
-
> The destroy half is **driver-specific**: node-postgres (`pg`) discards the poisoned connection
|
|
65
|
-
>
|
|
64
|
+
> The destroy half is **driver-specific**: node-postgres (`pg`) discards the poisoned connection.
|
|
65
|
+
> But postgres-js has no destroy semantics (`ReservedSql.release()` takes no argument), so under the
|
|
66
66
|
> postgres-js driver the connection returns to the pool anyway. See
|
|
67
67
|
> [Postgres Drivers & Supabase](./postgres-drivers) for the full asymmetry.
|
|
68
68
|
|
|
@@ -78,7 +78,7 @@ try {
|
|
|
78
78
|
| `commit()` | `Promise<void>` | Commit and release the connection. **Throws** if `COMMIT` fails, and destroys the connection rather than pooling it |
|
|
79
79
|
| `rollback()` | `Promise<void>` | Rollback and release the connection. **Throws** if `ROLLBACK` fails, and destroys the connection rather than pooling it |
|
|
80
80
|
|
|
81
|
-
Calling `commit()` or `rollback()` on an already-ended transaction throws an error
|
|
81
|
+
Calling `commit()` or `rollback()` on an already-ended transaction throws an error. The one exception: `rollback()` after the transaction ended BY FAILURE is a silent no-op (see the warning above).
|
|
82
82
|
|
|
83
83
|
## Isolation Levels
|
|
84
84
|
|
|
@@ -95,9 +95,10 @@ IGNIS supports standard PostgreSQL isolation levels:
|
|
|
95
95
|
|
|
96
96
|
## Best Practices
|
|
97
97
|
|
|
98
|
-
1. **Always use `try...catch`, and nest the rollback**: `rollback()` throws when `ROLLBACK` fails
|
|
99
|
-
2. **
|
|
100
|
-
3. **
|
|
98
|
+
1. **Always use `try...catch`, and nest the rollback**: `rollback()` throws when `ROLLBACK` fails. A bare `await tx.rollback()` inside a `catch` would discard the error that sent you there.
|
|
99
|
+
2. **Wrap the rollback in its own `try...catch`**: log the rollback failure, and rethrow the original cause.
|
|
100
|
+
3. **Keep it short**: Long-running transactions hold database connections from the pool and can cause connection exhaustion.
|
|
101
|
+
4. **Pass explicit options**: When calling other services inside a transaction, ensure they accept and use the `transaction` option.
|
|
101
102
|
|
|
102
103
|
```typescript
|
|
103
104
|
// Service method supporting transactions
|
|
@@ -196,7 +197,7 @@ export class OrderController extends BaseRestController {
|
|
|
196
197
|
|
|
197
198
|
## How Transactions Work Internally
|
|
198
199
|
|
|
199
|
-
When you pass a `transaction` option to a repository method, the repository uses the transaction's `connector`
|
|
200
|
+
When you pass a `transaction` option to a repository method, the repository uses the transaction's `connector` instead of the default datasource connector. That connector is a Drizzle instance bound to the transaction's dedicated connection. All operations within the transaction therefore use the same database connection and see a consistent view of the data.
|
|
200
201
|
|
|
201
202
|
```typescript
|
|
202
203
|
// Inside PostgresBaseRepository (simplified)
|