@zhin.js/database 1.1.4 → 1.1.6
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.md +41 -0
- package/lib/base/database.d.ts +4 -0
- package/lib/base/database.js +53 -9
- package/lib/base/transaction.js +10 -14
- package/lib/builtin-runtime.d.ts +2 -0
- package/lib/builtin-runtime.js +16 -0
- package/lib/dialects/memory.js +0 -2
- package/lib/dialects/mongodb.d.ts +10 -0
- package/lib/dialects/mongodb.js +0 -2
- package/lib/dialects/mysql.d.ts +10 -0
- package/lib/dialects/mysql.js +0 -2
- package/lib/dialects/pg.d.ts +10 -0
- package/lib/dialects/pg.js +1 -3
- package/lib/dialects/redis.d.ts +10 -0
- package/lib/dialects/redis.js +0 -2
- package/lib/dialects/sqlite.d.ts +3 -15
- package/lib/dialects/sqlite.js +29 -50
- package/lib/index.d.ts +1 -4
- package/lib/index.js +3 -4
- package/lib/registry.d.ts +5 -17
- package/lib/structured-schema.d.ts +10 -0
- package/lib/structured-schema.js +32 -0
- package/package.json +26 -1
package/README.md
CHANGED
|
@@ -16,6 +16,8 @@ npm install mysql2
|
|
|
16
16
|
|
|
17
17
|
# For PostgreSQL
|
|
18
18
|
npm install pg
|
|
19
|
+
# TypeScript projects selecting PostgreSQL types also install its declarations
|
|
20
|
+
npm install --save-dev @types/pg
|
|
19
21
|
|
|
20
22
|
# For MongoDB
|
|
21
23
|
npm install mongodb
|
|
@@ -24,6 +26,34 @@ npm install mongodb
|
|
|
24
26
|
npm install redis
|
|
25
27
|
```
|
|
26
28
|
|
|
29
|
+
## TypeScript entry points
|
|
30
|
+
|
|
31
|
+
The default entry exports the neutral database/model/transaction APIs and Memory/SQLite. External dialects expose their complete native driver options through their own entries:
|
|
32
|
+
|
|
33
|
+
| Dialect | Class and configuration entry |
|
|
34
|
+
| --- | --- |
|
|
35
|
+
| MySQL | `@zhin.js/database/dialects/mysql` |
|
|
36
|
+
| PostgreSQL | `@zhin.js/database/dialects/pg` |
|
|
37
|
+
| MongoDB | `@zhin.js/database/dialects/mongodb` |
|
|
38
|
+
| Redis | `@zhin.js/database/dialects/redis` |
|
|
39
|
+
|
|
40
|
+
Select the corresponding types before using a remote `Registry.create()` call:
|
|
41
|
+
|
|
42
|
+
```typescript
|
|
43
|
+
import { Registry } from '@zhin.js/database';
|
|
44
|
+
import type {} from '@zhin.js/database/dialects/pg';
|
|
45
|
+
|
|
46
|
+
const db = Registry.create('pg', { application_name: 'my-bot', statement_timeout: 1000 });
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Direct constructors are imported from the same dialect entry, for example `import { PG } from '@zhin.js/database/dialects/pg'`. They are no longer re-exported by the database, Core or Zhin default entry. SQLite users do not need the other four drivers or their declarations; JavaScript PostgreSQL users only need `pg`. `@types/pg` remains a build development dependency of this package, not a new optional peer.
|
|
50
|
+
|
|
51
|
+
All built-in factories still register when the default entry is loaded. `Registry.create()` remains synchronous; native clients are loaded when the selected database connects. Host YAML/JSON configuration is unchanged.
|
|
52
|
+
|
|
53
|
+
Third-party dialects augment only `@zhin.js/database/registry`: both `Databases<S>` and `Registry.Config` must describe their concrete class and configuration. The Registry value from this entry and from the default entry is the same initialized object. See the [type-isolation contract](../../docs/plans/database-optional-dialect-types.md).
|
|
54
|
+
|
|
55
|
+
These entries first require **`@zhin.js/database >=1.1.6` (pending release)**. Plugins using them must declare that minimum database dependency; existing root imports of external dialect classes must be migrated together.
|
|
56
|
+
|
|
27
57
|
## Quick Start
|
|
28
58
|
|
|
29
59
|
```typescript
|
|
@@ -125,6 +155,8 @@ MySQL 方言将 `text` 的非空默认值和 `json` 默认值写成表达式,
|
|
|
125
155
|
MySQL 或 mysql2,也无需删除已有数据库。修复针对新建表,不会缩窄既有文本列。
|
|
126
156
|
|
|
127
157
|
```typescript
|
|
158
|
+
import type {} from '@zhin.js/database/dialects/mysql';
|
|
159
|
+
|
|
128
160
|
const db = Registry.create('mysql', {
|
|
129
161
|
host: 'localhost',
|
|
130
162
|
port: 3306,
|
|
@@ -139,6 +171,8 @@ await db.start();
|
|
|
139
171
|
### PostgreSQL Example
|
|
140
172
|
|
|
141
173
|
```typescript
|
|
174
|
+
import type {} from '@zhin.js/database/dialects/pg';
|
|
175
|
+
|
|
142
176
|
const db = Registry.create('pg', {
|
|
143
177
|
host: 'localhost',
|
|
144
178
|
port: 5432,
|
|
@@ -153,6 +187,8 @@ await db.start();
|
|
|
153
187
|
### MongoDB Example
|
|
154
188
|
|
|
155
189
|
```typescript
|
|
190
|
+
import type {} from '@zhin.js/database/dialects/mongodb';
|
|
191
|
+
|
|
156
192
|
const db = Registry.create('mongodb', {
|
|
157
193
|
url: 'mongodb://localhost:27017',
|
|
158
194
|
dbName: 'myapp'
|
|
@@ -164,6 +200,8 @@ await db.start();
|
|
|
164
200
|
### Redis Example
|
|
165
201
|
|
|
166
202
|
```typescript
|
|
203
|
+
import type {} from '@zhin.js/database/dialects/redis';
|
|
204
|
+
|
|
167
205
|
const db = Registry.create('redis', {
|
|
168
206
|
socket: {
|
|
169
207
|
host: 'localhost',
|
|
@@ -934,6 +972,9 @@ if (db.isLogging) {
|
|
|
934
972
|
MySQL 和 PostgreSQL 支持连接池,提高高并发场景下的性能:
|
|
935
973
|
|
|
936
974
|
```typescript
|
|
975
|
+
import type {} from '@zhin.js/database/dialects/mysql';
|
|
976
|
+
import type {} from '@zhin.js/database/dialects/pg';
|
|
977
|
+
|
|
937
978
|
// MySQL 连接池配置
|
|
938
979
|
const db = Registry.create('mysql', {
|
|
939
980
|
host: 'localhost',
|
package/lib/base/database.d.ts
CHANGED
|
@@ -22,6 +22,10 @@ export declare abstract class Database<D = any, S extends Record<string, object>
|
|
|
22
22
|
readonly models: Database.Models<S, D, Q>;
|
|
23
23
|
private _logger?;
|
|
24
24
|
private _logging;
|
|
25
|
+
private readonly transactionContext;
|
|
26
|
+
private connectionTail;
|
|
27
|
+
/** 单连接事务独占连接;连接池仍允许不相关操作并行。 */
|
|
28
|
+
private acquireConnection;
|
|
25
29
|
constructor(dialect: Dialect<D, S, Q>, definitions?: Database.DefinitionObj<S>);
|
|
26
30
|
/**
|
|
27
31
|
* 若方言支持按列声明类型处理(如 SQLite 读路径的 json 反序列化),向其注册表结构
|
package/lib/base/database.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
1
2
|
import * as QueryClasses from './query-classes.js';
|
|
2
3
|
import { TransactionContextImpl } from './transaction.js';
|
|
3
4
|
import { getLogger } from '@zhin.js/logger';
|
|
@@ -21,6 +22,19 @@ export class Database {
|
|
|
21
22
|
models = new Database.Models();
|
|
22
23
|
_logger;
|
|
23
24
|
_logging = false;
|
|
25
|
+
transactionContext = new AsyncLocalStorage();
|
|
26
|
+
connectionTail = Promise.resolve();
|
|
27
|
+
/** 单连接事务独占连接;连接池仍允许不相关操作并行。 */
|
|
28
|
+
async acquireConnection() {
|
|
29
|
+
if (this.dialect.name !== 'sqlite'
|
|
30
|
+
&& (!['mysql', 'pg'].includes(this.dialect.name) || this.config?.pool))
|
|
31
|
+
return () => undefined;
|
|
32
|
+
const previous = this.connectionTail;
|
|
33
|
+
let release;
|
|
34
|
+
this.connectionTail = new Promise(resolve => { release = resolve; });
|
|
35
|
+
await previous;
|
|
36
|
+
return release;
|
|
37
|
+
}
|
|
24
38
|
constructor(dialect, definitions) {
|
|
25
39
|
this.dialect = dialect;
|
|
26
40
|
for (const key in definitions) {
|
|
@@ -108,9 +122,15 @@ export class Database {
|
|
|
108
122
|
if (!this.isStarted) {
|
|
109
123
|
throw new Error('Database not started');
|
|
110
124
|
}
|
|
125
|
+
const context = this.transactionContext.getStore();
|
|
126
|
+
if (context && !context.active)
|
|
127
|
+
throw new Error('Database transaction is already closed');
|
|
128
|
+
const release = context ? () => undefined : await this.acquireConnection();
|
|
111
129
|
const start = Date.now();
|
|
112
130
|
try {
|
|
113
|
-
const result =
|
|
131
|
+
const result = context
|
|
132
|
+
? await context.transaction.query(sql, params)
|
|
133
|
+
: await this.dialect.query(sql, params);
|
|
114
134
|
this.log(String(sql), params, Date.now() - start);
|
|
115
135
|
return result;
|
|
116
136
|
}
|
|
@@ -118,6 +138,9 @@ export class Database {
|
|
|
118
138
|
this.log(String(sql), params, Date.now() - start, error);
|
|
119
139
|
throw error;
|
|
120
140
|
}
|
|
141
|
+
finally {
|
|
142
|
+
release();
|
|
143
|
+
}
|
|
121
144
|
}
|
|
122
145
|
/**
|
|
123
146
|
* 获取数据库方言名称
|
|
@@ -198,16 +221,37 @@ export class Database {
|
|
|
198
221
|
if (!this.isStarted) {
|
|
199
222
|
throw new Error('Database not started');
|
|
200
223
|
}
|
|
201
|
-
|
|
202
|
-
|
|
224
|
+
if (this.transactionContext.getStore())
|
|
225
|
+
throw new Error('Nested database transactions are not supported');
|
|
226
|
+
const release = await this.acquireConnection();
|
|
203
227
|
try {
|
|
204
|
-
const
|
|
205
|
-
|
|
206
|
-
|
|
228
|
+
const trx = await this.dialect.beginTransaction(options);
|
|
229
|
+
const frame = { transaction: trx, active: true };
|
|
230
|
+
// 所有入口(含 model 查询)都绑定同一个事务,禁止异步逃逸后继续访问。
|
|
231
|
+
const guarded = {
|
|
232
|
+
query: (sql, params) => {
|
|
233
|
+
if (!frame.active)
|
|
234
|
+
return Promise.reject(new Error('Database transaction is already closed'));
|
|
235
|
+
return trx.query(sql, params);
|
|
236
|
+
},
|
|
237
|
+
commit: () => trx.commit(),
|
|
238
|
+
rollback: () => trx.rollback(),
|
|
239
|
+
};
|
|
240
|
+
const trxContext = new TransactionContextImpl(this, guarded);
|
|
241
|
+
try {
|
|
242
|
+
const result = await this.transactionContext.run(frame, () => callback(trxContext));
|
|
243
|
+
frame.active = false;
|
|
244
|
+
await trxContext.commit();
|
|
245
|
+
return result;
|
|
246
|
+
}
|
|
247
|
+
catch (error) {
|
|
248
|
+
frame.active = false;
|
|
249
|
+
await trxContext.rollback();
|
|
250
|
+
throw error;
|
|
251
|
+
}
|
|
207
252
|
}
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
throw error;
|
|
253
|
+
finally {
|
|
254
|
+
release();
|
|
211
255
|
}
|
|
212
256
|
}
|
|
213
257
|
/**
|
package/lib/base/transaction.js
CHANGED
|
@@ -32,7 +32,7 @@ class TrxSelection {
|
|
|
32
32
|
this.offsetCount = count;
|
|
33
33
|
return this;
|
|
34
34
|
}
|
|
35
|
-
then(onfulfilled) {
|
|
35
|
+
then(onfulfilled, onrejected) {
|
|
36
36
|
const { query, params } = this.db.buildQuery({
|
|
37
37
|
type: 'select',
|
|
38
38
|
tableName: this.tableName,
|
|
@@ -42,7 +42,7 @@ class TrxSelection {
|
|
|
42
42
|
limitCount: this.limitCount,
|
|
43
43
|
offsetCount: this.offsetCount
|
|
44
44
|
});
|
|
45
|
-
return this.trx.query(query, params).then(onfulfilled);
|
|
45
|
+
return this.trx.query(query, params).then(onfulfilled, onrejected);
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
48
|
/**
|
|
@@ -64,18 +64,15 @@ class TrxUpdation {
|
|
|
64
64
|
this.conditions = { ...this.conditions, ...condition };
|
|
65
65
|
return this;
|
|
66
66
|
}
|
|
67
|
-
then(onfulfilled) {
|
|
67
|
+
then(onfulfilled, onrejected) {
|
|
68
68
|
const { query, params } = this.db.buildQuery({
|
|
69
69
|
type: 'update',
|
|
70
70
|
tableName: this.tableName,
|
|
71
71
|
update: this.data,
|
|
72
72
|
conditions: this.conditions
|
|
73
73
|
});
|
|
74
|
-
return this.trx.query(query, params)
|
|
75
|
-
.then(
|
|
76
|
-
const affected = affectedRows(result);
|
|
77
|
-
return onfulfilled ? onfulfilled(affected) : affected;
|
|
78
|
-
});
|
|
74
|
+
return this.trx.query(this.db.dialectName === 'pg' ? `${query} RETURNING 1` : query, params)
|
|
75
|
+
.then(affectedRows).then(onfulfilled, onrejected);
|
|
79
76
|
}
|
|
80
77
|
}
|
|
81
78
|
/**
|
|
@@ -95,20 +92,19 @@ class TrxDeletion {
|
|
|
95
92
|
this.conditions = { ...this.conditions, ...condition };
|
|
96
93
|
return this;
|
|
97
94
|
}
|
|
98
|
-
then(onfulfilled) {
|
|
95
|
+
then(onfulfilled, onrejected) {
|
|
99
96
|
const { query, params } = this.db.buildQuery({
|
|
100
97
|
type: 'delete',
|
|
101
98
|
tableName: this.tableName,
|
|
102
99
|
conditions: this.conditions
|
|
103
100
|
});
|
|
104
|
-
return this.trx.query(query, params)
|
|
105
|
-
.then(
|
|
106
|
-
const affected = affectedRows(result);
|
|
107
|
-
return onfulfilled ? onfulfilled(affected) : affected;
|
|
108
|
-
});
|
|
101
|
+
return this.trx.query(this.db.dialectName === 'pg' ? `${query} RETURNING 1` : query, params)
|
|
102
|
+
.then(affectedRows).then(onfulfilled, onrejected);
|
|
109
103
|
}
|
|
110
104
|
}
|
|
111
105
|
function affectedRows(result) {
|
|
106
|
+
if (Array.isArray(result))
|
|
107
|
+
return result.length;
|
|
112
108
|
return typeof result === 'number'
|
|
113
109
|
? result
|
|
114
110
|
: result.affectedRows ?? result.changes ?? 0;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Registry } from './registry.js';
|
|
2
|
+
import { Memory } from './dialects/memory.js';
|
|
3
|
+
import { MySQL } from './dialects/mysql.js';
|
|
4
|
+
import { PG } from './dialects/pg.js';
|
|
5
|
+
import { Redis } from './dialects/redis.js';
|
|
6
|
+
import { Sqlite } from './dialects/sqlite.js';
|
|
7
|
+
import { MongoDB } from './dialects/mongodb.js';
|
|
8
|
+
/** Root-entry initialization only; vendor clients are still loaded on connect. */
|
|
9
|
+
export function registerBuiltinDatabases() {
|
|
10
|
+
Registry.register('memory', Memory);
|
|
11
|
+
Registry.register('mysql', MySQL);
|
|
12
|
+
Registry.register('pg', PG);
|
|
13
|
+
Registry.register('redis', Redis);
|
|
14
|
+
Registry.register('sqlite', Sqlite);
|
|
15
|
+
Registry.register('mongodb', MongoDB);
|
|
16
|
+
}
|
package/lib/dialects/memory.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { Dialect } from '../base/index.js';
|
|
2
2
|
import { RelatedDatabase } from "../type/related/database.js";
|
|
3
|
-
import { Registry } from "../registry.js";
|
|
4
3
|
function readSqlIdentifier(sql, start) {
|
|
5
4
|
let i = start;
|
|
6
5
|
while (i < sql.length && /\s/.test(sql[i]))
|
|
@@ -831,4 +830,3 @@ export class Memory extends RelatedDatabase {
|
|
|
831
830
|
super(new MemoryDialect(config), definitions);
|
|
832
831
|
}
|
|
833
832
|
}
|
|
834
|
-
Registry.register('memory', Memory);
|
|
@@ -87,3 +87,13 @@ export declare class MongoDBDialect<S extends Record<string, object> = Record<st
|
|
|
87
87
|
export declare class MongoDB<S extends Record<string, object> = Record<string, object>> extends DocumentDatabase<MongoDBDialectConfig, S> {
|
|
88
88
|
constructor(config: MongoDBDialectConfig, definitions?: Database.DefinitionObj<S>);
|
|
89
89
|
}
|
|
90
|
+
declare module '../registry.js' {
|
|
91
|
+
interface Databases<S extends Record<string, object> = Record<string, object>> {
|
|
92
|
+
mongodb: MongoDB<S>;
|
|
93
|
+
}
|
|
94
|
+
namespace Registry {
|
|
95
|
+
interface Config {
|
|
96
|
+
mongodb: MongoDBDialectConfig;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
package/lib/dialects/mongodb.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { Dialect } from '../base/index.js';
|
|
2
2
|
import { DocumentDatabase } from "../type/document/database.js";
|
|
3
|
-
import { Registry } from "../registry.js";
|
|
4
3
|
import { getLogger } from '@zhin.js/logger';
|
|
5
4
|
const logger = getLogger('database');
|
|
6
5
|
export class MongoDBDialect extends Dialect {
|
|
@@ -482,4 +481,3 @@ export class MongoDB extends DocumentDatabase {
|
|
|
482
481
|
super(new MongoDBDialect(config), definitions);
|
|
483
482
|
}
|
|
484
483
|
}
|
|
485
|
-
Registry.register('mongodb', MongoDB);
|
package/lib/dialects/mysql.d.ts
CHANGED
|
@@ -66,3 +66,13 @@ export declare class MySQL<S extends Record<string, object> = Record<string, obj
|
|
|
66
66
|
constructor(config: MySQLDialectConfig, definitions?: Database.DefinitionObj<S>);
|
|
67
67
|
protected initialize(): Promise<void>;
|
|
68
68
|
}
|
|
69
|
+
declare module '../registry.js' {
|
|
70
|
+
interface Databases<S extends Record<string, object> = Record<string, object>> {
|
|
71
|
+
mysql: MySQL<S>;
|
|
72
|
+
}
|
|
73
|
+
namespace Registry {
|
|
74
|
+
interface Config {
|
|
75
|
+
mysql: MySQLDialectConfig;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
package/lib/dialects/mysql.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { Dialect } from '../base/index.js';
|
|
2
2
|
import { RelatedDatabase } from "../type/related/database.js";
|
|
3
|
-
import { Registry } from "../registry.js";
|
|
4
3
|
import { getLogger } from '@zhin.js/logger';
|
|
5
4
|
const logger = getLogger('database');
|
|
6
5
|
export class MySQLDialect extends Dialect {
|
|
@@ -352,4 +351,3 @@ export class MySQL extends RelatedDatabase {
|
|
|
352
351
|
await super.initialize();
|
|
353
352
|
}
|
|
354
353
|
}
|
|
355
|
-
Registry.register('mysql', MySQL);
|
package/lib/dialects/pg.d.ts
CHANGED
|
@@ -60,3 +60,13 @@ export declare class PostgreSQLDialect<S extends Record<string, object> = Record
|
|
|
60
60
|
export declare class PG<S extends Record<string, object> = Record<string, object>> extends RelatedDatabase<PostgreSQLDialectConfig, S> {
|
|
61
61
|
constructor(config: PostgreSQLDialectConfig, definitions?: Database.DefinitionObj<S>);
|
|
62
62
|
}
|
|
63
|
+
declare module '../registry.js' {
|
|
64
|
+
interface Databases<S extends Record<string, object> = Record<string, object>> {
|
|
65
|
+
pg: PG<S>;
|
|
66
|
+
}
|
|
67
|
+
namespace Registry {
|
|
68
|
+
interface Config {
|
|
69
|
+
pg: PostgreSQLDialectConfig;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
package/lib/dialects/pg.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { Dialect } from '../base/index.js';
|
|
2
2
|
import { RelatedDatabase } from "../type/related/database.js";
|
|
3
|
-
import { Registry } from "../registry.js";
|
|
4
3
|
import { getLogger } from '@zhin.js/logger';
|
|
5
4
|
const logger = getLogger('database');
|
|
6
5
|
export class PostgreSQLDialect extends Dialect {
|
|
@@ -175,7 +174,7 @@ export class PostgreSQLDialect extends Dialect {
|
|
|
175
174
|
}
|
|
176
175
|
formatColumnDefinition(field, column) {
|
|
177
176
|
const name = this.quoteIdentifier(String(field));
|
|
178
|
-
const type = this.mapColumnType(column.type);
|
|
177
|
+
const type = column.type === 'text' && column.length ? 'VARCHAR' : this.mapColumnType(column.type);
|
|
179
178
|
const length = column.length ? `(${column.length})` : '';
|
|
180
179
|
const nullable = column.nullable === false ? ' NOT NULL' : '';
|
|
181
180
|
const primary = column.primary ? ' PRIMARY KEY' : '';
|
|
@@ -307,4 +306,3 @@ export class PG extends RelatedDatabase {
|
|
|
307
306
|
super(new PostgreSQLDialect(config), definitions);
|
|
308
307
|
}
|
|
309
308
|
}
|
|
310
|
-
Registry.register('pg', PG);
|
package/lib/dialects/redis.d.ts
CHANGED
|
@@ -88,3 +88,13 @@ export declare class RedisDialect<S extends Record<string, object> = Record<stri
|
|
|
88
88
|
export declare class Redis<S extends Record<string, object> = Record<string, object>> extends KeyValueDatabase<RedisDialectConfig, S> {
|
|
89
89
|
constructor(config: RedisDialectConfig, definitions?: Database.DefinitionObj<S>);
|
|
90
90
|
}
|
|
91
|
+
declare module '../registry.js' {
|
|
92
|
+
interface Databases<S extends Record<string, object> = Record<string, object>> {
|
|
93
|
+
redis: Redis<S>;
|
|
94
|
+
}
|
|
95
|
+
namespace Registry {
|
|
96
|
+
interface Config {
|
|
97
|
+
redis: RedisDialectConfig;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
package/lib/dialects/redis.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { Dialect } from '../base/index.js';
|
|
2
2
|
import { KeyValueDatabase } from "../type/keyvalue/database.js";
|
|
3
|
-
import { Registry } from "../registry.js";
|
|
4
3
|
import { getLogger } from '@zhin.js/logger';
|
|
5
4
|
const logger = getLogger('database');
|
|
6
5
|
export class RedisDialect extends Dialect {
|
|
@@ -502,4 +501,3 @@ export class Redis extends KeyValueDatabase {
|
|
|
502
501
|
super(new RedisDialect(config), definitions);
|
|
503
502
|
}
|
|
504
503
|
}
|
|
505
|
-
Registry.register('redis', Redis);
|
package/lib/dialects/sqlite.d.ts
CHANGED
|
@@ -16,27 +16,15 @@ export declare class SQLiteDialect<S extends Record<string, object> = Record<str
|
|
|
16
16
|
/** node:sqlite 仅支持 number/string/bigint/buffer/null;JSON 列在库内为 TEXT,对象需序列化 */
|
|
17
17
|
private prepareBindParams;
|
|
18
18
|
query<U = any>(sql: string, params?: any[]): Promise<U>;
|
|
19
|
-
/**
|
|
19
|
+
/** Column declarations are scoped to the table in this database, never the result alias. */
|
|
20
20
|
private readonly jsonColumns;
|
|
21
|
-
/**
|
|
22
|
-
* 注册表的列声明类型,用于读路径按声明类型反序列化(避免对 TEXT 列做启发式 JSON 解析破坏往返)
|
|
23
|
-
*/
|
|
24
21
|
registerTableSchema(table: string, definition: Record<string, {
|
|
25
22
|
type: string;
|
|
26
23
|
}>): void;
|
|
27
|
-
/**
|
|
28
|
-
*
|
|
29
|
-
*/
|
|
24
|
+
/** node:sqlite columns() is available since Node 22.16. It preserves table/column
|
|
25
|
+
* provenance through aliases and joins; expression results have no origin. */
|
|
30
26
|
private processQueryResults;
|
|
31
|
-
/**
|
|
32
|
-
* 处理单行数据
|
|
33
|
-
*/
|
|
34
27
|
private processRowData;
|
|
35
|
-
/**
|
|
36
|
-
* 处理字段值:仅对声明为 json 的列做 JSON 反序列化;
|
|
37
|
-
* 普通 TEXT 列原样返回,保证写入什么读出什么(不剥离引号、不启发式 JSON.parse)
|
|
38
|
-
*/
|
|
39
|
-
private processFieldValue;
|
|
40
28
|
dispose(): Promise<void>;
|
|
41
29
|
mapColumnType(type: string): string;
|
|
42
30
|
quoteIdentifier(identifier: string): string;
|
package/lib/dialects/sqlite.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import * as fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { Dialect } from '../base/index.js';
|
|
4
|
-
import { Registry } from '../registry.js';
|
|
5
4
|
import { RelatedDatabase } from '../type/related/database.js';
|
|
6
5
|
export class SQLiteDialect extends Dialect {
|
|
7
6
|
/** Node 内置 SQLite 连接(node:sqlite),在 connect() 中动态加载 */
|
|
@@ -82,62 +81,43 @@ export class SQLiteDialect extends Dialect {
|
|
|
82
81
|
// SELECT / WITH / PRAGMA / DDL / 带 RETURNING 的 DML:按可能有结果集处理。
|
|
83
82
|
// node:sqlite 的 all() 对无结果集语句返回空数组,不会抛错。
|
|
84
83
|
const rows = stmt.all(...args);
|
|
85
|
-
return this.processQueryResults(rows);
|
|
84
|
+
return this.processQueryResults(rows, stmt.columns());
|
|
86
85
|
}
|
|
87
|
-
/**
|
|
88
|
-
jsonColumns = new
|
|
89
|
-
/**
|
|
90
|
-
* 注册表的列声明类型,用于读路径按声明类型反序列化(避免对 TEXT 列做启发式 JSON 解析破坏往返)
|
|
91
|
-
*/
|
|
86
|
+
/** Column declarations are scoped to the table in this database, never the result alias. */
|
|
87
|
+
jsonColumns = new Map();
|
|
92
88
|
registerTableSchema(table, definition) {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
89
|
+
this.jsonColumns.set(table, new Set(Object.entries(definition)
|
|
90
|
+
.filter(([, column]) => column?.type === 'json').map(([name]) => name)));
|
|
91
|
+
}
|
|
92
|
+
/** node:sqlite columns() is available since Node 22.16. It preserves table/column
|
|
93
|
+
* provenance through aliases and joins; expression results have no origin. */
|
|
94
|
+
processQueryResults(data, columns) {
|
|
95
|
+
const decoded = new Set();
|
|
96
|
+
for (const column of columns) {
|
|
97
|
+
// Registered model definitions describe the main database. Attached tables
|
|
98
|
+
// with the same name do not inherit their schema.
|
|
99
|
+
if (column.database === 'main' && column.table && column.column
|
|
100
|
+
&& this.jsonColumns.get(column.table)?.has(column.column))
|
|
101
|
+
decoded.add(column.name);
|
|
102
|
+
else
|
|
103
|
+
decoded.delete(column.name);
|
|
97
104
|
}
|
|
105
|
+
return Array.isArray(data) ? data.map(row => this.processRowData(row, decoded)) : data;
|
|
98
106
|
}
|
|
99
|
-
|
|
100
|
-
* 处理查询结果,按列声明类型反序列化(json 列)
|
|
101
|
-
*/
|
|
102
|
-
processQueryResults(data) {
|
|
103
|
-
if (!data)
|
|
104
|
-
return data;
|
|
105
|
-
if (Array.isArray(data)) {
|
|
106
|
-
return data.map(row => this.processRowData(row));
|
|
107
|
-
}
|
|
108
|
-
else if (typeof data === 'object') {
|
|
109
|
-
return this.processRowData(data);
|
|
110
|
-
}
|
|
111
|
-
return data;
|
|
112
|
-
}
|
|
113
|
-
/**
|
|
114
|
-
* 处理单行数据
|
|
115
|
-
*/
|
|
116
|
-
processRowData(row) {
|
|
107
|
+
processRowData(row, decoded) {
|
|
117
108
|
if (!row || typeof row !== 'object')
|
|
118
109
|
return row;
|
|
119
|
-
const
|
|
110
|
+
const result = {};
|
|
120
111
|
for (const [key, value] of Object.entries(row)) {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
*/
|
|
129
|
-
processFieldValue(key, value) {
|
|
130
|
-
if (typeof value !== 'string')
|
|
131
|
-
return value;
|
|
132
|
-
if (!this.jsonColumns.has(key))
|
|
133
|
-
return value;
|
|
134
|
-
try {
|
|
135
|
-
return JSON.parse(value);
|
|
136
|
-
}
|
|
137
|
-
catch {
|
|
138
|
-
// 解析失败,返回原始字符串
|
|
139
|
-
return value;
|
|
112
|
+
result[key] = value;
|
|
113
|
+
if (decoded.has(key) && typeof value === 'string') {
|
|
114
|
+
try {
|
|
115
|
+
result[key] = JSON.parse(value);
|
|
116
|
+
}
|
|
117
|
+
catch { /* Preserve invalid JSON exactly as before. */ }
|
|
118
|
+
}
|
|
140
119
|
}
|
|
120
|
+
return result;
|
|
141
121
|
}
|
|
142
122
|
async dispose() {
|
|
143
123
|
if (this.db) {
|
|
@@ -274,4 +254,3 @@ export class Sqlite extends RelatedDatabase {
|
|
|
274
254
|
super(new SQLiteDialect(config), definitions);
|
|
275
255
|
}
|
|
276
256
|
}
|
|
277
|
-
Registry.register('sqlite', Sqlite);
|
package/lib/index.d.ts
CHANGED
|
@@ -10,9 +10,6 @@ export * from './type/keyvalue/database.js';
|
|
|
10
10
|
export * from './type/keyvalue/model.js';
|
|
11
11
|
export * from './registry.js';
|
|
12
12
|
export * from './dialects/memory.js';
|
|
13
|
-
export * from './dialects/mysql.js';
|
|
14
|
-
export * from './dialects/pg.js';
|
|
15
|
-
export * from './dialects/redis.js';
|
|
16
13
|
export * from './dialects/sqlite.js';
|
|
17
|
-
export * from './dialects/mongodb.js';
|
|
18
14
|
export * from './migration.js';
|
|
15
|
+
export * from './structured-schema.js';
|
package/lib/index.js
CHANGED
|
@@ -10,9 +10,8 @@ export * from './type/keyvalue/database.js';
|
|
|
10
10
|
export * from './type/keyvalue/model.js';
|
|
11
11
|
export * from './registry.js';
|
|
12
12
|
export * from './dialects/memory.js';
|
|
13
|
-
export * from './dialects/mysql.js';
|
|
14
|
-
export * from './dialects/pg.js';
|
|
15
|
-
export * from './dialects/redis.js';
|
|
16
13
|
export * from './dialects/sqlite.js';
|
|
17
|
-
export * from './dialects/mongodb.js';
|
|
18
14
|
export * from './migration.js';
|
|
15
|
+
export * from './structured-schema.js';
|
|
16
|
+
import { registerBuiltinDatabases } from './builtin-runtime.js';
|
|
17
|
+
registerBuiltinDatabases();
|
package/lib/registry.d.ts
CHANGED
|
@@ -1,20 +1,12 @@
|
|
|
1
|
-
import { Database } from './base/database.js';
|
|
2
|
-
import { Memory } from './dialects/memory.js';
|
|
3
|
-
import {
|
|
4
|
-
import { MySQL } from "./dialects/mysql.js";
|
|
5
|
-
import { PG } from "./dialects/pg.js";
|
|
6
|
-
import { Redis } from "./dialects/redis.js";
|
|
7
|
-
import { Sqlite } from "./dialects/sqlite.js";
|
|
1
|
+
import type { Database } from './base/database.js';
|
|
2
|
+
import type { Memory, MemoryConfig } from './dialects/memory.js';
|
|
3
|
+
import type { Sqlite, SQLiteDialectConfig } from './dialects/sqlite.js';
|
|
8
4
|
/**
|
|
9
5
|
* 数据库注册表接口
|
|
10
6
|
* 支持模块声明扩展
|
|
11
7
|
*/
|
|
12
8
|
export interface Databases<S extends Record<string, object> = Record<string, object>> {
|
|
13
9
|
memory: Memory<S>;
|
|
14
|
-
mongodb: MongoDB<S>;
|
|
15
|
-
mysql: MySQL<S>;
|
|
16
|
-
pg: PG<S>;
|
|
17
|
-
redis: Redis<S>;
|
|
18
10
|
sqlite: Sqlite<S>;
|
|
19
11
|
}
|
|
20
12
|
/**
|
|
@@ -26,12 +18,8 @@ export type Factory<D, S extends Record<string, object>, R extends Database<D, S
|
|
|
26
18
|
export declare namespace Registry {
|
|
27
19
|
const factories: Map<string, Factory<any, any, any>>;
|
|
28
20
|
interface Config {
|
|
29
|
-
memory:
|
|
30
|
-
|
|
31
|
-
mysql: import('./dialects/mysql.js').MySQLDialectConfig;
|
|
32
|
-
pg: import('./dialects/pg.js').PostgreSQLDialectConfig;
|
|
33
|
-
redis: import('./dialects/redis.js').RedisDialectConfig;
|
|
34
|
-
sqlite: import('./dialects/sqlite.js').SQLiteDialectConfig;
|
|
21
|
+
memory: MemoryConfig;
|
|
22
|
+
sqlite: SQLiteDialectConfig;
|
|
35
23
|
}
|
|
36
24
|
type DatabaseType = 'related' | 'document' | 'keyvalue';
|
|
37
25
|
function register<S extends Record<string, object>, D extends keyof Databases<S>>(dialect: D, factory: Factory<Config[D], S, any>): void;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Database } from './base/database.js';
|
|
2
|
+
export type StructuredDatabase = Database<unknown, Record<string, Record<string, unknown>>, string>;
|
|
3
|
+
import type { Column } from './types.js';
|
|
4
|
+
/** Explicit additive migrations for a declared table; never removes old columns/data. */
|
|
5
|
+
export declare function ensureDatabaseColumns(database: StructuredDatabase, table: string, columns: Record<string, Column>): Promise<void>;
|
|
6
|
+
/** Index migration with exact identifier quoting; MySQL TEXT needs a bounded prefix. */
|
|
7
|
+
export declare function ensureDatabaseIndex(database: StructuredDatabase, table: string, name: string, columns: readonly {
|
|
8
|
+
name: string;
|
|
9
|
+
text?: boolean;
|
|
10
|
+
}[]): Promise<void>;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/** Explicit additive migrations for a declared table; never removes old columns/data. */
|
|
2
|
+
export async function ensureDatabaseColumns(database, table, columns) {
|
|
3
|
+
const dialect = database.dialectName;
|
|
4
|
+
if (!['sqlite', 'mysql', 'pg'].includes(dialect))
|
|
5
|
+
return;
|
|
6
|
+
const quote = (name) => dialect === 'mysql' ? `\`${name.replaceAll('`', '``')}\`` : `"${name.replaceAll('"', '""')}"`;
|
|
7
|
+
const rows = dialect === 'sqlite'
|
|
8
|
+
? await database.query(`PRAGMA table_info(${quote(table)})`)
|
|
9
|
+
: dialect === 'mysql' ? await database.query(`SHOW COLUMNS FROM ${quote(table)}`)
|
|
10
|
+
: await database.query('SELECT column_name FROM information_schema.columns WHERE table_schema = current_schema() AND table_name = $1', [table]);
|
|
11
|
+
if (!rows.length)
|
|
12
|
+
throw new Error(`Cannot migrate missing table '${table}'`);
|
|
13
|
+
const existing = new Set(rows.map(row => String(row.name ?? row.Field ?? row.column_name)));
|
|
14
|
+
for (const [name, definition] of Object.entries(columns)) {
|
|
15
|
+
if (!existing.has(name))
|
|
16
|
+
await database.alter(table, { [name]: { action: 'add', ...definition } });
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/** Index migration with exact identifier quoting; MySQL TEXT needs a bounded prefix. */
|
|
20
|
+
export async function ensureDatabaseIndex(database, table, name, columns) {
|
|
21
|
+
const dialect = database.dialectName;
|
|
22
|
+
if (!['sqlite', 'mysql', 'pg'].includes(dialect))
|
|
23
|
+
return;
|
|
24
|
+
const quote = (value) => dialect === 'mysql' ? `\`${value.replaceAll('`', '``')}\`` : `"${value.replaceAll('"', '""')}"`;
|
|
25
|
+
if (dialect === 'mysql') {
|
|
26
|
+
const rows = await database.query(`SHOW INDEX FROM ${quote(table)} WHERE Key_name = ?`, [name]);
|
|
27
|
+
if (rows.length)
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const fields = columns.map(column => quote(column.name) + (dialect === 'mysql' && column.text ? '(128)' : '')).join(', ');
|
|
31
|
+
await database.query(`CREATE INDEX ${dialect === 'mysql' ? '' : 'IF NOT EXISTS '}${quote(name)} ON ${quote(table)} (${fields})`);
|
|
32
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhin.js/database",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.6",
|
|
4
4
|
"description": "Universal database abstraction layer for Zhin.js - supports SQLite, MySQL, PostgreSQL, MongoDB, Redis",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|
|
@@ -10,6 +10,31 @@
|
|
|
10
10
|
"types": "./lib/index.d.ts",
|
|
11
11
|
"import": "./lib/index.js",
|
|
12
12
|
"require": "./lib/index.js"
|
|
13
|
+
},
|
|
14
|
+
"./registry": {
|
|
15
|
+
"types": "./lib/registry.d.ts",
|
|
16
|
+
"import": "./lib/index.js",
|
|
17
|
+
"require": "./lib/index.js"
|
|
18
|
+
},
|
|
19
|
+
"./dialects/mysql": {
|
|
20
|
+
"types": "./lib/dialects/mysql.d.ts",
|
|
21
|
+
"import": "./lib/dialects/mysql.js",
|
|
22
|
+
"require": "./lib/dialects/mysql.js"
|
|
23
|
+
},
|
|
24
|
+
"./dialects/pg": {
|
|
25
|
+
"types": "./lib/dialects/pg.d.ts",
|
|
26
|
+
"import": "./lib/dialects/pg.js",
|
|
27
|
+
"require": "./lib/dialects/pg.js"
|
|
28
|
+
},
|
|
29
|
+
"./dialects/mongodb": {
|
|
30
|
+
"types": "./lib/dialects/mongodb.d.ts",
|
|
31
|
+
"import": "./lib/dialects/mongodb.js",
|
|
32
|
+
"require": "./lib/dialects/mongodb.js"
|
|
33
|
+
},
|
|
34
|
+
"./dialects/redis": {
|
|
35
|
+
"types": "./lib/dialects/redis.d.ts",
|
|
36
|
+
"import": "./lib/dialects/redis.js",
|
|
37
|
+
"require": "./lib/dialects/redis.js"
|
|
13
38
|
}
|
|
14
39
|
},
|
|
15
40
|
"files": [
|