@h-ai/reldb 1.0.0-beta.19 → 1.0.0-beta.20
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/README.en.md +225 -141
- package/README.ja.md +225 -141
- package/README.md +225 -141
- package/package.json +2 -2
package/README.en.md
CHANGED
|
@@ -4,33 +4,9 @@
|
|
|
4
4
|
|
|
5
5
|
[Framework home](../../README.en.md) · Related modules: [@h-ai/core](../core/README.en.md)
|
|
6
6
|
|
|
7
|
-
## Introduction
|
|
8
|
-
|
|
9
7
|
A relational database module with a unified `reldb` object for SQLite, PostgreSQL, and MySQL.
|
|
10
8
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
```ts
|
|
14
|
-
const closed = await reldb.close()
|
|
15
|
-
if (!closed.success) {
|
|
16
|
-
// Retry once after addressing the cleanup failure; report another failure.
|
|
17
|
-
const retried = await reldb.close()
|
|
18
|
-
if (!retried.success)
|
|
19
|
-
throw new Error(retried.error.message)
|
|
20
|
-
}
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
Close waits for borrowed transactions to commit/roll back and release their handles; applications must finish transactions for shutdown to complete. SQLite WAL setup and worker handshakes are part of the owned initialization chain. PostgreSQL removes disconnected idle clients and replaces them on later queries without terminating the process; logs contain safe error identifiers rather than raw driver errors.
|
|
24
|
-
|
|
25
|
-
## Core features
|
|
26
|
-
|
|
27
|
-
- Connect to SQLite, PostgreSQL and MySQL through one API.
|
|
28
|
-
- Run parameterized SQL, manage tables/indexes and read paginated results.
|
|
29
|
-
- Organize application reads and writes with transactions, CRUD and repositories.
|
|
30
|
-
|
|
31
|
-
## Getting started
|
|
32
|
-
|
|
33
|
-
### Quick start
|
|
9
|
+
## Quick Start
|
|
34
10
|
|
|
35
11
|
```ts
|
|
36
12
|
import { HaiReldbError, reldb } from '@h-ai/reldb'
|
|
@@ -75,141 +51,105 @@ await reldb.close()
|
|
|
75
51
|
|
|
76
52
|
`reldb.config` returns a **redacted configuration snapshot**. Database `password`, URL credentials and the TLS client private key `ssl.key` are hidden. Use it to inspect state, not to replay connection details.
|
|
77
53
|
|
|
78
|
-
|
|
54
|
+
## Features
|
|
79
55
|
|
|
80
|
-
|
|
56
|
+
Connections become available only after a successful health check. Failed initialization disposes its candidate; `error.ext.cleanupPending === true` identifies cleanup that still needs retry. `reldb.close()` returns `Promise<HaiResult<void>>`, waits for initialization, and coalesces concurrent close calls. Once cleanup starts, `isInitialized` is false and configuration/operations are unavailable. A failed cleanup retains the resource: the next `close()` or `init()` retries disposal before allocating a replacement. Always inspect the close result.
|
|
81
57
|
|
|
82
58
|
```ts
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
59
|
+
const closed = await reldb.close()
|
|
60
|
+
if (!closed.success) {
|
|
61
|
+
// Retry once after addressing the cleanup failure; report another failure.
|
|
62
|
+
const retried = await reldb.close()
|
|
63
|
+
if (!retried.success)
|
|
64
|
+
throw new Error(retried.error.message)
|
|
65
|
+
}
|
|
89
66
|
```
|
|
90
67
|
|
|
91
|
-
|
|
68
|
+
Close waits for borrowed transactions to commit/roll back and release their handles; applications must finish transactions for shutdown to complete. SQLite WAL setup and worker handshakes are part of the owned initialization chain. PostgreSQL removes disconnected idle clients and replaces them on later queries without terminating the process; logs contain safe error identifiers rather than raw driver errors.
|
|
92
69
|
|
|
93
|
-
|
|
94
|
-
// SQLite
|
|
95
|
-
await reldb.init({ type: 'sqlite', database: './data.db' })
|
|
70
|
+
### Operations and results
|
|
96
71
|
|
|
97
|
-
|
|
98
|
-
await reldb.init({ type: 'postgresql', url: 'postgres://user:pass@localhost:5432/mydb', database: 'mydb' })
|
|
72
|
+
Use the complete initialization flow in this section first. The host validates and authorizes example business values; check success before reading data from asynchronous domain results. Here void means completion without data, null means absent; synchronous factories/tools return the listed type directly.
|
|
99
73
|
|
|
100
|
-
|
|
101
|
-
await reldb.init({
|
|
102
|
-
type: 'mysql',
|
|
103
|
-
host: 'localhost',
|
|
104
|
-
port: 3306,
|
|
105
|
-
database: 'mydb',
|
|
106
|
-
user: 'admin',
|
|
107
|
-
password: 'secret',
|
|
108
|
-
mysql: { charset: 'utf8mb4' },
|
|
109
|
-
pool: { max: 10, acquireTimeout: 5000, queueLimit: 100 },
|
|
110
|
-
})
|
|
111
|
-
```
|
|
74
|
+
Table/column/index names and SQL come from trusted code; params bind business values, steps define module migrations and options control preview/pagination.
|
|
112
75
|
|
|
113
|
-
####
|
|
76
|
+
#### reldb.ddl
|
|
114
77
|
|
|
115
|
-
|
|
78
|
+
Maintain tables, columns and indexes. Supply trusted identifiers, column/index definitions or DDL; success contains void, invalid changes fail.
|
|
116
79
|
|
|
117
|
-
|
|
|
118
|
-
|
|
|
119
|
-
| `
|
|
120
|
-
| `
|
|
121
|
-
| `
|
|
122
|
-
| `
|
|
123
|
-
| `
|
|
80
|
+
| Entry | Input parameters | Successful result | Call example |
|
|
81
|
+
| ----------------------- | -------------------------------- | ----------------- | ------------------------------------------------------------------------------------ |
|
|
82
|
+
| `reldb.ddl.createTable` | tableName, columns, ifNotExists | `void` | `await reldb.ddl.createTable('orders', { id: { type: 'TEXT', primaryKey: true } })` |
|
|
83
|
+
| `reldb.ddl.dropTable` | tableName, ifExists | `void` | `await reldb.ddl.dropTable('orders_archive', true)` |
|
|
84
|
+
| `reldb.ddl.addColumn` | tableName, columnName, columnDef | `void` | `await reldb.ddl.addColumn('orders', 'note', { type: 'TEXT' })` |
|
|
85
|
+
| `reldb.ddl.dropColumn` | tableName, columnName | `void` | `await reldb.ddl.dropColumn('orders', 'note')` |
|
|
86
|
+
| `reldb.ddl.renameTable` | oldName, newName | `void` | `await reldb.ddl.renameTable('orders_draft', 'orders')` |
|
|
87
|
+
| `reldb.ddl.createIndex` | tableName, indexName, indexDef | `void` | `await reldb.ddl.createIndex('orders', 'orders_status', { columns: ['status'] })` |
|
|
88
|
+
| `reldb.ddl.dropIndex` | indexName, ifExists | `void` | `await reldb.ddl.dropIndex('orders_status', true)` |
|
|
89
|
+
| `reldb.ddl.raw` | sql | `void` | `await reldb.ddl.raw('CREATE TABLE IF NOT EXISTS app_marker (id TEXT PRIMARY KEY)')` |
|
|
124
90
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
MySQL `close()` rejects new and queued requests immediately, then waits for borrowed connections, transactions and pending establishment before ending the pool. The driver uses the same value for `connectTimeout`; shutdown may wait for that timeout or existing work to finish. Applications must finish transactions. Environment variables use `HAI_DB_POOL_*`: `MIN` and `IDLETIMEOUT` apply only to PostgreSQL; MySQL `QUEUELIMIT` bounds queued requests.
|
|
91
|
+
#### reldb.sql
|
|
128
92
|
|
|
129
|
-
|
|
93
|
+
Read/write business data with bound params: rows, first row/null, write result, void or a page respectively; batch takes statement objects.
|
|
130
94
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
})
|
|
137
|
-
|
|
138
|
-
throw new Error(initialized.error.message)
|
|
139
|
-
```
|
|
95
|
+
| Entry | Input parameters | Successful result | Call example |
|
|
96
|
+
| --------------------- | ---------------- | -------------------- | ---------------------------------------------------------------------------------------------------------------- |
|
|
97
|
+
| `reldb.sql.query` | sql, params | `T[]` | `await reldb.sql.query('SELECT id FROM orders WHERE status = ?', ['pending'])` |
|
|
98
|
+
| `reldb.sql.get` | sql, params | `T \| null` | `await reldb.sql.get('SELECT id FROM orders WHERE id = ?', ['o1'])` |
|
|
99
|
+
| `reldb.sql.execute` | sql, params | `ExecuteResult` | `await reldb.sql.execute('UPDATE orders SET status = ? WHERE id = ?', ['paid', 'o1'])` |
|
|
100
|
+
| `reldb.sql.batch` | statements | `void` | `await reldb.sql.batch([{ sql: 'DELETE FROM orders WHERE id = ?', params: ['o1'] }])` |
|
|
101
|
+
| `reldb.sql.queryPage` | options | `PaginatedResult<T>` | `await reldb.sql.queryPage({ sql: 'SELECT id FROM orders ORDER BY id', pagination: { page: 1, pageSize: 20 } })` |
|
|
140
102
|
|
|
141
|
-
|
|
103
|
+
#### reldb.pagination
|
|
142
104
|
|
|
143
|
-
|
|
105
|
+
Pagination: normalize accepts page options and limits and returns normalized options; build accepts items, total and paging and returns the page object directly.
|
|
144
106
|
|
|
145
|
-
|
|
107
|
+
| Entry | Input parameters | Successful result | Call example |
|
|
108
|
+
| ---------------------------- | ------------------------ | ---------------------- | ---------------------------------------------------------- |
|
|
109
|
+
| `reldb.pagination.normalize` | options, overrides | `NormalizedPagination` | `reldb.pagination.normalize({ page: 1, pageSize: 20 })` |
|
|
110
|
+
| `reldb.pagination.build` | items, total, pagination | `PaginatedResult<T>` | `reldb.pagination.build([], 0, { page: 1, pageSize: 20 })` |
|
|
146
111
|
|
|
147
|
-
|
|
112
|
+
#### reldb
|
|
148
113
|
|
|
149
|
-
|
|
150
|
-
// Supply trustedCaPem from deployment trust configuration, not an arbitrary peer certificate.
|
|
151
|
-
const initialized = await reldb.init({
|
|
152
|
-
type: 'mysql',
|
|
153
|
-
host: 'db.example.com',
|
|
154
|
-
database: 'app',
|
|
155
|
-
ssl: { ca: trustedCaPem }
|
|
156
|
-
})
|
|
157
|
-
if (!initialized.success)
|
|
158
|
-
throw new Error(initialized.error.message)
|
|
159
|
-
```
|
|
114
|
+
Prepare module persistence: table name returns columns; module name, steps and dryRun return a migration plan. Back up and stop conflicting writers before applying.
|
|
160
115
|
|
|
161
|
-
|
|
116
|
+
| Entry | Input parameters | Successful result | Call example |
|
|
117
|
+
| --------------------- | ---------------------- | --------------------- | ----------------------------------------------------------------------- |
|
|
118
|
+
| `reldb.schemaColumns` | table, sql | `string[]` | `await reldb.schemaColumns('orders')` |
|
|
119
|
+
| `reldb.migrateSchema` | module, steps, options | `SchemaMigrationPlan` | `await reldb.migrateSchema('orders', migrationSteps, { dryRun: true })` |
|
|
162
120
|
|
|
163
|
-
####
|
|
121
|
+
#### reldb.tx
|
|
164
122
|
|
|
165
|
-
|
|
123
|
+
Atomic writes: begin yields a transaction with query/get/execute/batch/queryPage/commit/rollback; wrap runs a callback and returns its value. A failed HaiResult must explicitly trigger rollback.
|
|
166
124
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
sqlite: {
|
|
172
|
-
executor: 'worker', // 'sync'(默认)| 'worker'
|
|
173
|
-
walMode: true, // 默认 true
|
|
174
|
-
workerConnectTimeoutMs: 10000, // worker 打开连接超时,默认 10000
|
|
175
|
-
},
|
|
176
|
-
})
|
|
177
|
-
```
|
|
125
|
+
| Entry | Input parameters | Successful result | Call example |
|
|
126
|
+
| ---------------- | ---------------- | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
127
|
+
| `reldb.tx.begin` | No arguments | `DmlWithTxOperations` | `await reldb.tx.begin()` |
|
|
128
|
+
| `reldb.tx.wrap` | fn | `T` | `await reldb.tx.wrap(async tx => { const found = await tx.get('SELECT 1 AS value'); if (!found.success) throw new Error(found.error.message); return found.data })` |
|
|
178
129
|
|
|
179
|
-
|
|
130
|
+
#### reldb.crud
|
|
180
131
|
|
|
181
|
-
|
|
132
|
+
Supply table/field mappings for a business entity; obtain a repository directly for CRUD and pagination without exposing a database connection.
|
|
182
133
|
|
|
183
|
-
|
|
134
|
+
| Entry | Input parameters | Successful result | Call example |
|
|
135
|
+
| ------------------ | ---------------- | ---------------------------- | ----------------------------------------- |
|
|
136
|
+
| `reldb.crud.table` | config | `ReldbCrudRepository<TItem>` | `reldb.crud.table(orderRepositoryConfig)` |
|
|
184
137
|
|
|
185
|
-
|
|
186
|
-
# config/_db.yml
|
|
187
|
-
type: sqlite
|
|
188
|
-
database: ./data/app.db
|
|
189
|
-
operationLog:
|
|
190
|
-
read: false # query / get / queryPage
|
|
191
|
-
write: false # DDL, execute / batch, tx.begin / tx.commit / tx.rollback
|
|
192
|
-
maxLength: 1000 # maximum numeric summary length; 0 omits the summary, SQL text and values are never logged
|
|
193
|
-
level: debug # info | debug | trace,默认 debug
|
|
194
|
-
```
|
|
138
|
+
#### reldb.json
|
|
195
139
|
|
|
196
|
-
|
|
197
|
-
`HAI_DB_DATABASE`, and `HAI_DB_OPERATIONLOG_LEVEL` override YAML values.
|
|
140
|
+
Cross-dialect JSON expressions: trusted column, JSON path and value/patch return SQL plus bound params directly, without executing a query.
|
|
198
141
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
maxLength: 500,
|
|
207
|
-
level: 'debug',
|
|
208
|
-
},
|
|
209
|
-
})
|
|
210
|
-
```
|
|
142
|
+
| Entry | Input parameters | Successful result | Call example |
|
|
143
|
+
| -------------------- | ------------------- | ----------------- | --------------------------------------------------- |
|
|
144
|
+
| `reldb.json.extract` | column, path | `JsonSqlExpr` | `reldb.json.extract('payload', '$.status')` |
|
|
145
|
+
| `reldb.json.set` | column, path, value | `JsonSqlExpr` | `reldb.json.set('payload', '$.status', 'paid')` |
|
|
146
|
+
| `reldb.json.insert` | column, path, value | `JsonSqlExpr` | `reldb.json.insert('payload', '$.note', 'checked')` |
|
|
147
|
+
| `reldb.json.remove` | column, path | `JsonSqlExpr` | `reldb.json.remove('payload', '$.note')` |
|
|
148
|
+
| `reldb.json.merge` | column, patch | `JsonSqlExpr` | `reldb.json.merge('payload', { status: 'paid' })` |
|
|
211
149
|
|
|
212
|
-
|
|
150
|
+
- Connect to SQLite, PostgreSQL and MySQL through one API.
|
|
151
|
+
- Run parameterized SQL, manage tables/indexes and read paginated results.
|
|
152
|
+
- Organize application reads and writes with transactions, CRUD and repositories.
|
|
213
153
|
|
|
214
154
|
### Transactions
|
|
215
155
|
|
|
@@ -659,7 +599,159 @@ const { sql: rmSql, params: rmParams } = reldb.json.remove('meta', '$.reviewer')
|
|
|
659
599
|
await reldb.sql.execute(`UPDATE products SET meta = ${rmSql} WHERE id = ?`, [...rmParams, 1])
|
|
660
600
|
```
|
|
661
601
|
|
|
662
|
-
###
|
|
602
|
+
### Production shared state
|
|
603
|
+
|
|
604
|
+
Production requires PostgreSQL/MySQL. SQLite, including disk files, is for development/explicit tests and does not provide cross-host shared state. Initialization checks apply when NODE_ENV or HAI_CORE_ENV is production; only explicit HAI_E2E=1/HAI_E2E_MOCK=1 tests are exempt. [State, concurrency and recovery (Chinese)](../../.agents/instructions/distributed-state.md).
|
|
605
|
+
|
|
606
|
+
### Module schema migrations
|
|
607
|
+
|
|
608
|
+
`reldb.schemaColumns(table, sql?)` inspects real columns. `reldb.migrateSchema(module, steps, { dryRun: true })` creates no ledger. Modules own their steps. Execution journals versions in `hai_schema_migrations`; SQLite/PostgreSQL are transactional, while MySQL implicit DDL commits require reentrant steps. Failures do not advance versions; retries inspect real state. Older code rejects newer ledgers. Stop old writers and back up DB/session cache first. Rollback restores compatible database and binaries together. See the [upgrade guide](../../docs/usage/README.en.md).
|
|
609
|
+
|
|
610
|
+
## Configuration
|
|
611
|
+
|
|
612
|
+
Lifecycle: init takes backend/business configuration; use the module only after success. Await close before releasing borrowed dependencies. Follow the quick-start example and public signatures for exact return values.
|
|
613
|
+
|
|
614
|
+
`@h-ai/reldb/schemas` exports the same configuration schemas and types with only Zod, without loading the database singleton or drivers. CLI and browser code can validate configuration through this entry. MySQL TLS accepts `false` or a strict object; `{}` verifies the certificate chain and DNS identity by default. PostgreSQL retains its boolean, string mode and object contract.
|
|
615
|
+
|
|
616
|
+
```ts
|
|
617
|
+
import { ReldbConfigSchema } from '@h-ai/reldb/schemas'
|
|
618
|
+
|
|
619
|
+
const checked = ReldbConfigSchema.safeParse({ type: 'mysql', database: 'shop', ssl: {} })
|
|
620
|
+
if (!checked.success)
|
|
621
|
+
throw new Error('Invalid database configuration')
|
|
622
|
+
// checked.data.ssl: { rejectUnauthorized: true, verifyIdentity: true }
|
|
623
|
+
```
|
|
624
|
+
|
|
625
|
+
Use either a connection string (`url`) or separate fields (`host/port/database/user/password`).
|
|
626
|
+
|
|
627
|
+
```ts
|
|
628
|
+
// SQLite
|
|
629
|
+
await reldb.init({ type: 'sqlite', database: './data.db' })
|
|
630
|
+
|
|
631
|
+
// PostgreSQL
|
|
632
|
+
await reldb.init({ type: 'postgresql', url: 'postgres://user:pass@localhost:5432/mydb', database: 'mydb' })
|
|
633
|
+
|
|
634
|
+
// MySQL
|
|
635
|
+
await reldb.init({
|
|
636
|
+
type: 'mysql',
|
|
637
|
+
host: 'localhost',
|
|
638
|
+
port: 3306,
|
|
639
|
+
database: 'mydb',
|
|
640
|
+
user: 'admin',
|
|
641
|
+
password: 'secret',
|
|
642
|
+
mysql: { charset: 'utf8mb4' },
|
|
643
|
+
pool: { max: 10, acquireTimeout: 5000, queueLimit: 100 },
|
|
644
|
+
})
|
|
645
|
+
```
|
|
646
|
+
|
|
647
|
+
### Connection pools and MySQL URLs
|
|
648
|
+
|
|
649
|
+
MySQL `pool` accepts only `max`, `acquireTimeout` and `queueLimit`, defaulting to 10, 10000 milliseconds and 1000 even when omitted. Unsupported pool fields such as `min` and `idleTimeout`, and the unimplemented `mysql.timezone`, fail configuration validation. Idle connections remain subject to mysql2/server disconnection and `close()`; the module promises no MySQL minimum retention or configurable idle expiry. [mysql2 3.24.5 options](https://github.com/sidorares/node-mysql2/blob/v3.24.5/typings/mysql/lib/Pool.d.ts) differ from PostgreSQL.
|
|
650
|
+
|
|
651
|
+
| Field | PostgreSQL (defaults when `pool` is provided) | MySQL |
|
|
652
|
+
| ---------------- | ------------------------------------------------------------- | --------------------------------------------------- |
|
|
653
|
+
| `max` | 10 | 10, counting borrowed and connecting slots together |
|
|
654
|
+
| `min` | 1, retention floor for established connections; no prewarming | Unsupported; rejected |
|
|
655
|
+
| `idleTimeout` | 30000 ms; 0 disables | Unsupported; rejected |
|
|
656
|
+
| `acquireTimeout` | 10000 ms; 0 disables | 10000 ms; integer from 1 to 2147483647 |
|
|
657
|
+
| `queueLimit` | Unsupported | 1000; positive integer limiting queued requests |
|
|
658
|
+
|
|
659
|
+
PostgreSQL uses pg defaults when `pool` is omitted; see [pg.Pool](https://node-postgres.com/apis/pool) for retention semantics. All MySQL SQL, DDL, CRUD, batch and transaction entry points use a bounded FIFO acquisition queue. Waiting and establishment share one deadline: expiry returns `TIMEOUT`, and a full queue returns `POOL_EXHAUSTED`. Expired waiters are removed; late connections are released without executing SQL and retain their capacity slot until settled. The deadline is checked on delivery and before starting work. It does not limit SQL or transactions that have already started.
|
|
660
|
+
|
|
661
|
+
MySQL `close()` rejects new and queued requests immediately, then waits for borrowed connections, transactions and pending establishment before ending the pool. The driver uses the same value for `connectTimeout`; shutdown may wait for that timeout or existing work to finish. Applications must finish transactions. Environment variables use `HAI_DB_POOL_*`: `MIN` and `IDLETIMEOUT` apply only to PostgreSQL; MySQL `QUEUELIMIT` bounds queued requests.
|
|
662
|
+
|
|
663
|
+
MySQL accepts a `mysql://` URL without split connection fields; `database` is required without a URL. URL host/port/database/user/password take precedence over conflicting split fields. `pool`, `mysql.charset`, `ssl` and `operationLog` remain independent options. Query strings and fragments are rejected, so hidden URL parameters cannot change TLS or pool policy. Connection logs sanitize URLs. This example assumes `HAI_DB_URL` is set:
|
|
664
|
+
|
|
665
|
+
```ts
|
|
666
|
+
const initialized = await reldb.init({
|
|
667
|
+
type: 'mysql',
|
|
668
|
+
url: process.env.HAI_DB_URL,
|
|
669
|
+
pool: { max: 10, acquireTimeout: 5000, queueLimit: 100 }
|
|
670
|
+
})
|
|
671
|
+
if (!initialized.success)
|
|
672
|
+
throw new Error(initialized.error.message)
|
|
673
|
+
```
|
|
674
|
+
|
|
675
|
+
An empty MySQL `url` (including an empty environment placeholder) selects host/port/database/user/password; nonempty URLs remain strictly validated.
|
|
676
|
+
|
|
677
|
+
#### MySQL TLS
|
|
678
|
+
|
|
679
|
+
Omitting `ssl` or using `false` disables TLS. Enabling it requires a strict object; an empty object defaults to `{ rejectUnauthorized: true, verifyIdentity: true }`. `ca` contains trusted CA PEM text, or Node's trust store is used when omitted. Optional client `cert` and `key` must contain PEM text and be provided together. Unknown TLS fields, `true` and PostgreSQL string modes are rejected; arbitrary options and callbacks are not forwarded.
|
|
680
|
+
|
|
681
|
+
[mysql2 3.24.5 identity checks](https://github.com/sidorares/node-mysql2/blob/v3.24.5/lib/base/connection.js) depend on a DNS servername. With both certificate and identity checks enabled, IP targets return `CONFIG_ERROR`; use a hostname matching the certificate DNS SAN. Explicit `verifyIdentity: false` retains only chain verification. `rejectUnauthorized: false` abandons server authentication; encryption alone does not authenticate the server. Disabling either check emits a fixed risk warning and is not reported as a secure configuration.
|
|
682
|
+
|
|
683
|
+
```ts
|
|
684
|
+
// Supply trustedCaPem from deployment trust configuration, not an arbitrary peer certificate.
|
|
685
|
+
const initialized = await reldb.init({
|
|
686
|
+
type: 'mysql',
|
|
687
|
+
host: 'db.example.com',
|
|
688
|
+
database: 'app',
|
|
689
|
+
ssl: { ca: trustedCaPem }
|
|
690
|
+
})
|
|
691
|
+
if (!initialized.success)
|
|
692
|
+
throw new Error(initialized.error.message)
|
|
693
|
+
```
|
|
694
|
+
|
|
695
|
+
TLS tests require OpenSSL (Git for Windows includes one) and local container port forwarding. Each run generates and removes its own CA/private keys and checks trusted DNS, wrong CA/hostname rejection, `REQUIRE X509` client authentication and actual session `Ssl_cipher`. Disabling verification is not accepted as proof of authenticated TLS.
|
|
696
|
+
|
|
697
|
+
#### SQLite execution (asynchronous worker)
|
|
698
|
+
|
|
699
|
+
better-sqlite3 is synchronous, so queries block Node's main event loop. WAL improves database lock concurrency but cannot remove this blocking. To offload it, opt in to a dedicated SQLite worker thread (single connection; defaults to `sync`):
|
|
700
|
+
|
|
701
|
+
```ts
|
|
702
|
+
await reldb.init({
|
|
703
|
+
type: 'sqlite',
|
|
704
|
+
database: './data.db',
|
|
705
|
+
sqlite: {
|
|
706
|
+
executor: 'worker', // 'sync'(默认)| 'worker'
|
|
707
|
+
walMode: true, // 默认 true
|
|
708
|
+
workerConnectTimeoutMs: 10000, // worker 打开连接超时,默认 10000
|
|
709
|
+
},
|
|
710
|
+
})
|
|
711
|
+
```
|
|
712
|
+
|
|
713
|
+
In worker mode, queries, writes, and transactions run serially on the worker thread. Public APIs and return values remain identical to synchronous mode.
|
|
714
|
+
|
|
715
|
+
#### Operation logging
|
|
716
|
+
|
|
717
|
+
Enable SQL operation logs with `operationLog` in the database configuration. Logs are emitted at the DML layer of reldb provider-base, without duplicate wrappers in `reldb-main.ts` or raw providers.
|
|
718
|
+
|
|
719
|
+
```yaml
|
|
720
|
+
# config/_db.yml
|
|
721
|
+
type: sqlite
|
|
722
|
+
database: ./data/app.db
|
|
723
|
+
operationLog:
|
|
724
|
+
read: false # query / get / queryPage
|
|
725
|
+
write: false # DDL, execute / batch, tx.begin / tx.commit / tx.rollback
|
|
726
|
+
maxLength: 1000 # maximum numeric summary length; 0 omits the summary, SQL text and values are never logged
|
|
727
|
+
level: debug # info | debug | trace,默认 debug
|
|
728
|
+
```
|
|
729
|
+
|
|
730
|
+
When loading through `core.config.load('db', ...)`, convention-based variables such as `HAI_DB_TYPE`,
|
|
731
|
+
`HAI_DB_DATABASE`, and `HAI_DB_OPERATIONLOG_LEVEL` override YAML values.
|
|
732
|
+
|
|
733
|
+
```ts
|
|
734
|
+
await reldb.init({
|
|
735
|
+
type: 'sqlite',
|
|
736
|
+
database: './data.db',
|
|
737
|
+
operationLog: {
|
|
738
|
+
read: true,
|
|
739
|
+
write: true,
|
|
740
|
+
maxLength: 500,
|
|
741
|
+
level: 'debug',
|
|
742
|
+
},
|
|
743
|
+
})
|
|
744
|
+
```
|
|
745
|
+
|
|
746
|
+
Operation logs contain fixed operation names and SQL length, parameter count, and batch count. Failure logs also retain error codes, traceId, and allowlisted, sanitized driver messages and stacks for diagnosis. Full SQL, bound parameters, and connection configuration are not logged directly.
|
|
747
|
+
|
|
748
|
+
The host loads and validates configuration before passing it to this module. Keep secrets in private server environment files. Select the backend, capacity, deadlines and logging for the actual use case; reinitialize and check the result after changes.
|
|
749
|
+
|
|
750
|
+
[Per-option purpose, use cases and examples](../../docs/usage/configuration/data.md).
|
|
751
|
+
|
|
752
|
+
## Error Handling
|
|
753
|
+
|
|
754
|
+
Check result.success for domain operations. Handle failures with the module error definitions and safe messages; retain codes, correlation IDs and sanitized causes in server logs. Synchronous tools, factories, stream iteration and lifecycle calls follow their own signatures. A completed Promise alone is not business success. See each feature for recovery limits.
|
|
663
755
|
|
|
664
756
|
All operations return `HaiResult<T>`; check `result.success` for success or failure.
|
|
665
757
|
|
|
@@ -698,15 +790,7 @@ Common error codes:
|
|
|
698
790
|
|
|
699
791
|
Custom SQL methods in `BaseReldbCrudRepository` subclasses must check `await this.ensureReady()`. Construction starts table creation; service init must await candidate readiness before publication. PostgreSQL retries a single autocommit `CREATE TABLE/INDEX IF NOT EXISTS` once only for `23505/pg_class_relname_nsp_index` catalog contention (also `pg_type_typname_nsp_index` for tables), or a table row-type race reported as `42710` with server routine `heap_create_with_catalog` / `TypeCreate`, consistently through DDL and SQL entry points. A failed retry remains an error. Transactions, business uniqueness errors and other failures are not retried. Initial connection probing attempts temporary DNS `EAI_AGAIN` failures at most three times, with 250/500 ms delays; business SQL is never replayed.
|
|
700
792
|
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
Production requires PostgreSQL/MySQL. SQLite, including disk files, is for development/explicit tests and does not provide cross-host shared state. Initialization checks apply when NODE_ENV or HAI_CORE_ENV is production; only explicit HAI_E2E=1/HAI_E2E_MOCK=1 tests are exempt. [State, concurrency and recovery (Chinese)](../../.agents/instructions/distributed-state.md)。
|
|
704
|
-
|
|
705
|
-
## Module schema migrations
|
|
706
|
-
|
|
707
|
-
`reldb.schemaColumns(table, sql?)` inspects real columns. `reldb.migrateSchema(module, steps, { dryRun: true })` creates no ledger. Modules own their steps. Execution journals versions in `hai_schema_migrations`; SQLite/PostgreSQL are transactional, while MySQL implicit DDL commits require reentrant steps. Failures do not advance versions; retries inspect real state. Older code rejects newer ledgers. Stop old writers and back up DB/session cache first. Rollback restores compatible database and binaries together. See the [upgrade guide](../../docs/usage/README.en.md).
|
|
708
|
-
|
|
709
|
-
## Error messages and correlated logs
|
|
793
|
+
### Error messages and correlated logs
|
|
710
794
|
|
|
711
795
|
Unique constraint violations return `HaiReldbError.DUPLICATE_ENTRY` (409); timeout, pool exhaustion and other existing error codes remain intact. Branch on error codes rather than localized messages or driver text. Logs preserve driver code/schema/routine/constraint and stack line numbers; numeric bound values do not obscure these diagnostic identifiers.
|
|
712
796
|
|
|
@@ -714,7 +798,7 @@ HTTP errors carry safe localized messages and a reference ID. ToastContainer hid
|
|
|
714
798
|
|
|
715
799
|
Use `core.error.toPublic(error, traceId?)` at HTTP/UI boundaries and `core.error.formatMessage(error)` after translating messages; formatting is idempotent. Pass complete Error objects to logs instead of discarding their context. Database failures log driver codes, sanitized messages/stacks and table/column context; SQL text, bound values and connection credentials are excluded.
|
|
716
800
|
|
|
717
|
-
## Development
|
|
801
|
+
## Development
|
|
718
802
|
|
|
719
803
|
```bash
|
|
720
804
|
pnpm --filter @h-ai/reldb test
|