@zhin.js/database 1.1.7 → 1.2.0

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 CHANGED
@@ -8,7 +8,7 @@ Universal database abstraction layer for Zhin.js framework with support for mult
8
8
  npm install @zhin.js/database
9
9
  ```
10
10
 
11
- Install the database driver you need(SQLite 使用 Node 内置 `node:sqlite`,无需安装,需 Node.js 22.5+,推荐 24+):
11
+ Install the database driver you need(SQLite 使用 Node 内置 `node:sqlite`,无需安装,需 Node.js >=22.19.0,推荐 24+):
12
12
 
13
13
  ```bash
14
14
  # For MySQL
@@ -83,7 +83,7 @@ const user = await userModel.create({
83
83
  ## Supported Databases
84
84
 
85
85
  ### ✅ Relational Databases (已完整实现)
86
- - **SQLite** - 内置支持,使用 Node 自带 `node:sqlite`(无需安装驱动,需 Node.js 22.5+,推荐 24+)
86
+ - **SQLite** - 内置支持,使用 Node 自带 `node:sqlite`(无需安装驱动,需 Node.js >=22.19.0,推荐 24+)
87
87
  - 轻量级、零配置
88
88
  - 适合中小型应用
89
89
  - 支持 WAL 模式
@@ -262,27 +262,33 @@ await db.start();
262
262
 
263
263
  ## Model API
264
264
 
265
+ 以下为关系型模型(`RelatedModel`,SQLite / MySQL / PostgreSQL)的便捷方法;更复杂的查询见下方链式查询 API。
266
+
265
267
  ```typescript
266
268
  const model = db.model('users');
267
269
 
268
270
  // Create
269
271
  const user = await model.create({ name: 'John', email: 'john@example.com' });
272
+ const created = await model.createMany([{ name: 'A' }, { name: 'B' }]);
270
273
 
271
274
  // Read
272
- const users = await model.find({ name: 'John' });
273
- const user = await model.findOne({ email: 'john@example.com' });
275
+ const users = await model.findAll({ name: 'John' });
276
+ const one = await model.findOne({ email: 'john@example.com' });
277
+ const byId = await model.findById(1);
274
278
 
275
- // Update
276
- await model.update({ name: 'John' }, { name: 'Jane' });
279
+ // Update —— 条件在前、数据在后;返回受影响行数
280
+ await model.updateWhere({ name: 'John' }, { name: 'Jane' });
281
+ await model.updateById(1, { name: 'Jane' });
277
282
 
278
- // Delete
279
- await model.remove({ name: 'Jane' });
283
+ // Delete —— 启用软删除的模型会改为标记 deletedAt
284
+ await model.deleteWhere({ name: 'Jane' });
285
+ await model.deleteById(1);
280
286
 
281
287
  // Count
282
288
  const count = await model.count({ email: { $like: '%@example.com' } });
283
289
 
284
- // Pagination
285
- const result = await model.find({}, { limit: 10, offset: 0 });
290
+ // Pagination —— 用链式查询
291
+ const page = await model.select().where({}).limit(10).offset(0);
286
292
  ```
287
293
 
288
294
  ## 链式查询 API (Fluent Query Builder)
@@ -236,6 +236,13 @@ export class Database {
236
236
  },
237
237
  commit: () => trx.commit(),
238
238
  rollback: () => trx.rollback(),
239
+ ...(trx.queryStatement ? {
240
+ queryStatement: (sql, options) => {
241
+ if (!frame.active)
242
+ return Promise.reject(new Error('Database transaction is already closed'));
243
+ return trx.queryStatement(sql, options);
244
+ },
245
+ } : {}),
239
246
  };
240
247
  const trxContext = new TransactionContextImpl(this, guarded);
241
248
  try {
@@ -0,0 +1,5 @@
1
+ /** Row-cap helpers of `Transaction.queryStatement` (internal, not part of the public entry). */
2
+ /** Marks a row array as cut at `maxRows` (non-enumerable, so JSON and equality ignore it). */
3
+ export declare function markTruncated<T extends unknown[]>(rows: T, truncated: boolean): T;
4
+ /** Applies {@link StatementQueryOptions.maxRows} to a fully read result. */
5
+ export declare function capRows<T>(result: T, maxRows: number | undefined): T;
@@ -0,0 +1,16 @@
1
+ /** Row-cap helpers of `Transaction.queryStatement` (internal, not part of the public entry). */
2
+ /** Marks a row array as cut at `maxRows` (non-enumerable, so JSON and equality ignore it). */
3
+ export function markTruncated(rows, truncated) {
4
+ if (Array.isArray(rows) && !Object.isFrozen(rows))
5
+ Object.defineProperty(rows, 'truncated', { value: truncated, enumerable: false, configurable: true });
6
+ return rows;
7
+ }
8
+ /** Applies {@link StatementQueryOptions.maxRows} to a fully read result. */
9
+ export function capRows(result, maxRows) {
10
+ if (maxRows === undefined || !Array.isArray(result))
11
+ return result;
12
+ const extra = result.length > maxRows;
13
+ if (extra)
14
+ result.splice(maxRows);
15
+ return markTruncated(result, extra);
16
+ }
@@ -1,4 +1,4 @@
1
- import { Transaction, TransactionContext, TransactionSelection, TransactionUpdation, TransactionDeletion } from '../types.js';
1
+ import { Transaction, TransactionContext, TransactionSelection, TransactionUpdation, TransactionDeletion, StatementQueryOptions } from '../types.js';
2
2
  import type { Database } from './database.js';
3
3
  /**
4
4
  * 增强的事务上下文实现
@@ -20,6 +20,8 @@ export declare class TransactionContextImpl<S extends Record<string, object>> im
20
20
  * 执行原生 SQL
21
21
  */
22
22
  query<T = any>(sql: string, params?: any[]): Promise<T>;
23
+ /** One statement through the single-statement protocol of the dialect (see {@link Transaction.queryStatement}). */
24
+ queryStatement<T = any>(sql: string, options?: StatementQueryOptions): Promise<T>;
23
25
  /**
24
26
  * 插入单条数据
25
27
  */
@@ -138,6 +138,12 @@ export class TransactionContextImpl {
138
138
  async query(sql, params) {
139
139
  return this.trx.query(sql, params);
140
140
  }
141
+ /** One statement through the single-statement protocol of the dialect (see {@link Transaction.queryStatement}). */
142
+ async queryStatement(sql, options) {
143
+ if (!this.trx.queryStatement)
144
+ throw new Error('This database does not support single-statement queries');
145
+ return this.trx.queryStatement(sql, options);
146
+ }
141
147
  /**
142
148
  * 插入单条数据
143
149
  */
@@ -0,0 +1,181 @@
1
+ export type ConsoleCredentialDialect = 'pg' | 'mysql';
2
+ /** Runs one statement on a connection (parameters bound by the driver). */
3
+ export type ConsoleSqlRunner = (sql: string, params?: unknown[]) => Promise<unknown>;
4
+ /** Connections the restricted account may hold at once (the Console pool uses at most 2). */
5
+ export declare const CONSOLE_ROLE_CONNECTION_LIMIT = 4;
6
+ /** Role-level `statement_timeout` backstop (the Console sets a shorter one per transaction). */
7
+ export declare const CONSOLE_ROLE_STATEMENT_TIMEOUT = "60s";
8
+ /** Privileges of the PostgreSQL account on tables / sequences of `public`. */
9
+ export declare const PG_CONSOLE_TABLE_PRIVILEGES = "SELECT, INSERT, UPDATE, DELETE, TRUNCATE";
10
+ export declare const PG_CONSOLE_SEQUENCE_PRIVILEGES = "USAGE, SELECT, UPDATE";
11
+ /** Privileges of the MySQL account on `<database>.*` (nothing global). */
12
+ export declare const MYSQL_CONSOLE_PRIVILEGES: readonly ["SELECT", "INSERT", "UPDATE", "DELETE", "CREATE", "ALTER", "DROP", "INDEX", "REFERENCES", "CREATE VIEW", "SHOW VIEW", "CREATE TEMPORARY TABLES"];
13
+ /** Schema privileges the MySQL account must never hold. */
14
+ export declare const MYSQL_FORBIDDEN_PRIVILEGES: ReadonlySet<string>;
15
+ /**
16
+ * Role-level settings of the PostgreSQL account (`pg_db_role_setting`), exactly: the probe refuses
17
+ * an account whose settings differ — the account itself may `ALTER ROLE current_user SET …`
18
+ * (e.g. `search_path = pg_temp, public`), which would change how the main account's objects resolve
19
+ * in its sessions.
20
+ */
21
+ export declare const PG_CONSOLE_ROLE_SETTINGS: readonly string[];
22
+ /** SCRAM-SHA-256 iteration count of the verifier sent instead of the password (PostgreSQL default). */
23
+ export declare const PG_SCRAM_ITERATIONS = 4096;
24
+ /**
25
+ * A PostgreSQL SCRAM-SHA-256 verifier (`SCRAM-SHA-256$<iter>:<salt>$<StoredKey>:<ServerKey>`, RFC
26
+ * 7677 / `pg_authid.rolpassword` format) computed on the client, so `CREATE` / `ALTER ROLE` never
27
+ * carries the password itself — not to the server log, not into `--print-sql` output. The password
28
+ * is base64url ASCII, so SASLprep leaves it unchanged.
29
+ */
30
+ export declare function pgScramVerifier(password: string, salt?: Buffer, iterations?: number): string;
31
+ /** Replaces every spelling of `secret` (raw, quoted, URL-encoded) in `text` with `[redacted]`. */
32
+ export declare function scrubSecret(text: string, secrets: readonly (string | undefined)[]): string;
33
+ /** An error with `secrets` scrubbed from its message (driver codes kept; the original is dropped). */
34
+ export declare function scrubbedError(error: unknown, secrets: readonly (string | undefined)[]): Error;
35
+ /** `zhin_console_<first 8 hex of SHA-256(projectKey)>`: one account per project. */
36
+ export declare function consoleRoleName(projectKey: string): string;
37
+ /** 32 random bytes, base64url (no quotes or backslashes: safe inside a SQL literal). */
38
+ export declare function generateConsolePassword(): string;
39
+ export declare function quotePgIdentifier(name: string): string;
40
+ export interface ConsoleRoleStatementOptions {
41
+ readonly dialect: ConsoleCredentialDialect;
42
+ readonly role: string;
43
+ /** The account's password; `--print-sql` passes a placeholder the DBA replaces. */
44
+ readonly password: string;
45
+ /** The project database. */
46
+ readonly database: string;
47
+ /** The main account (PostgreSQL: default privileges are declared for the tables it creates). */
48
+ readonly mainUser: string;
49
+ /** MySQL: host part of the new account (the host part of the main account's `CURRENT_USER()`). */
50
+ readonly host?: string;
51
+ /** PostgreSQL: the role exists already (ALTER instead of CREATE). */
52
+ readonly exists?: boolean;
53
+ /** PostgreSQL: the main account is a superuser (may set SUPERUSER / REPLICATION / BYPASSRLS attributes on ALTER). */
54
+ readonly mainIsSuperuser?: boolean;
55
+ /** PostgreSQL: the main account has CREATEDB (PG 16 lets only such an account name CREATEDB on ALTER). */
56
+ readonly mainCanCreateDb?: boolean;
57
+ /**
58
+ * PostgreSQL: tables (and views) of `public` to grant on. Undefined grants `ON ALL TABLES IN
59
+ * SCHEMA public`; gateway-owned names (`console_*`) are always left out.
60
+ */
61
+ readonly tables?: readonly string[];
62
+ /** PostgreSQL ≤ 14: `PUBLIC` may create in `public`; revoke that (and grant CREATE to the main account first). */
63
+ readonly revokePublicCreate?: boolean;
64
+ }
65
+ /** The statements that create (or rotate) the restricted account, in order. */
66
+ export declare function consoleRoleStatements(options: ConsoleRoleStatementOptions): string[];
67
+ /** `user@host` of MySQL `CURRENT_USER()` (the user part may itself contain `@`). */
68
+ export declare function splitMysqlAccount(account: string): {
69
+ user: string;
70
+ host: string;
71
+ };
72
+ export interface ProvisionConsoleRoleOptions {
73
+ readonly dialect: ConsoleCredentialDialect;
74
+ /** Role name; default {@link consoleRoleName}(projectKey). */
75
+ readonly role?: string;
76
+ /** Stable project identity for the default role name (the CLI passes the project root). */
77
+ readonly projectKey: string;
78
+ /** Default {@link generateConsolePassword}. */
79
+ readonly password?: string;
80
+ }
81
+ export interface ConsoleRolePlan {
82
+ readonly dialect: ConsoleCredentialDialect;
83
+ readonly user: string;
84
+ readonly password: string;
85
+ /** MySQL host part of the account. */
86
+ readonly host?: string;
87
+ readonly database: string;
88
+ readonly mainUser: string;
89
+ readonly statements: readonly string[];
90
+ }
91
+ /**
92
+ * Reads what the statements need from the main account's connection (its name, the database,
93
+ * existing tables, whether the role exists) and returns the plan without running it
94
+ * (`--print-sql`).
95
+ */
96
+ export declare function planConsoleRole(run: ConsoleSqlRunner, options: ProvisionConsoleRoleOptions): Promise<ConsoleRolePlan>;
97
+ /**
98
+ * Creates (or rotates the password of) the restricted account with the main account's
99
+ * connection. Needs CREATEROLE (PostgreSQL) or CREATE USER + GRANT OPTION (MySQL); without them
100
+ * the first statement fails and the caller offers `--print-sql`.
101
+ */
102
+ export declare function provisionConsoleRole(run: ConsoleSqlRunner, options: ProvisionConsoleRoleOptions): Promise<ConsoleRolePlan>;
103
+ /**
104
+ * Version check of `CREATE USER IF NOT EXISTS` / `ALTER USER … IDENTIFIED BY` (MySQL 5.7.6,
105
+ * MariaDB 10.1.3); `undefined` when supported.
106
+ */
107
+ export declare function mysqlVersionProblem(version: string): string | undefined;
108
+ /** Role grants and PROXY grants in `SHOW GRANTS` output (`GRANT \`r\`@\`%\` TO …`, `GRANT PROXY ON …`). */
109
+ export declare function mysqlRoleAndProxyGrants(lines: readonly string[]): {
110
+ readonly roles: readonly string[];
111
+ readonly proxies: readonly string[];
112
+ };
113
+ export interface ConsoleCredentialProbe {
114
+ readonly ok: boolean;
115
+ /** Why the account is not acceptable (empty when ok). */
116
+ readonly problems: readonly string[];
117
+ /** The account the connection logged in as. */
118
+ readonly user?: string;
119
+ /**
120
+ * For the log only (never shown to Console users): object names behind a problem, e.g. the
121
+ * SECURITY DEFINER functions the account may execute.
122
+ */
123
+ readonly details?: readonly string[];
124
+ }
125
+ /** How many SECURITY DEFINER functions a probe names in its log details. */
126
+ export declare const PG_DEFINER_FUNCTIONS_LOGGED = 5;
127
+ /**
128
+ * The account may EXECUTE a SECURITY DEFINER function owned by another role outside `pg_catalog` /
129
+ * `information_schema` (extension functions in other schemas included): it would run with that
130
+ * owner's rights. PUBLIC has EXECUTE on functions by default, so provisioning cannot fix this
131
+ * without changing everyone's access — the owner revokes it (or grants it explicitly) instead.
132
+ */
133
+ export declare const PG_DEFINER_FUNCTION_PROBLEM: string;
134
+ /**
135
+ * Functions that read or write server files or reach other servers (`pg_ls_*`, `pg_read_file`,
136
+ * `pg_read_binary_file`, `pg_stat_file`, server-side `lo_import` / `lo_export`, adminpack
137
+ * `pg_file_*`, `dblink*`): none of them may be executable by the console account (they are
138
+ * revoked from PUBLIC by default; an explicit grant or an extension that grants them is refused).
139
+ */
140
+ export declare const PG_FILE_FUNCTIONS_QUERY = "SELECT DISTINCT p.proname AS name FROM pg_proc p\n WHERE (p.proname LIKE 'pg\\_ls\\_%' OR p.proname IN ('pg_read_file', 'pg_read_binary_file', 'pg_stat_file', 'lo_import', 'lo_export')\n OR p.proname LIKE 'pg\\_file\\_%' OR p.proname LIKE 'dblink%')\n AND has_function_privilege(current_user, p.oid, 'EXECUTE')\n ORDER BY 1";
141
+ /** The query behind {@link PG_DEFINER_FUNCTION_PROBLEM} (at most PG_DEFINER_FUNCTIONS_LOGGED + 1 rows). */
142
+ export declare const PG_DEFINER_FUNCTIONS_QUERY = "SELECT n.nspname AS schema, p.proname AS name FROM pg_proc p\n JOIN pg_namespace n ON n.oid = p.pronamespace\n WHERE p.prosecdef AND p.proowner <> (SELECT oid FROM pg_roles WHERE rolname = current_user)\n AND n.nspname NOT IN ('pg_catalog', 'information_schema') AND n.nspname NOT LIKE 'pg\\_temp\\_%' AND n.nspname NOT LIKE 'pg\\_toast\\_temp\\_%'\n AND has_function_privilege(current_user, p.oid, 'EXECUTE')\n ORDER BY 1, 2 LIMIT 6";
143
+ export interface ProbeConsoleCredentialOptions {
144
+ readonly dialect: ConsoleCredentialDialect;
145
+ /** `database.console.user`. */
146
+ readonly expectedUser: string;
147
+ /** The main account (`database.user`; PostgreSQL also the main connection's `current_user`). */
148
+ readonly mainUser?: string;
149
+ }
150
+ /**
151
+ * Checks, on the restricted connection itself, that it is the configured account and holds no
152
+ * more than {@link consoleRoleStatements} grants. Raw Console SQL stays off unless `ok`.
153
+ */
154
+ export declare function probeConsoleCredential(run: ConsoleSqlRunner, options: ProbeConsoleCredentialOptions): Promise<ConsoleCredentialProbe>;
155
+ export interface RefreshConsoleGrantsOptions {
156
+ readonly role: string;
157
+ }
158
+ /**
159
+ * PostgreSQL, main account: grants the account the table / sequence privileges on what exists in
160
+ * `public` now (tables created before the default privileges, or by another role the main
161
+ * account can grant for), and revokes everything on gateway-owned (`console_*`) tables that are
162
+ * still in the project database. Idempotent; run at instance start.
163
+ */
164
+ export declare function refreshConsoleGrants(run: ConsoleSqlRunner, options: RefreshConsoleGrantsOptions): Promise<{
165
+ readonly granted: readonly string[];
166
+ readonly revoked: readonly string[];
167
+ }>;
168
+ export interface ConsoleCredential {
169
+ readonly user: string;
170
+ readonly password: string;
171
+ }
172
+ /**
173
+ * Connection options of the restricted Console account, built from the main connection's options
174
+ * (ADR 0059 §4): the main account's credentials never reach it — neither as fields nor inside a
175
+ * PostgreSQL `connectionString` (whose `user` / `password`, also as query parameters, would win over
176
+ * explicit fields, empty ones included) nor a MySQL `uri` (whose query parameters fill every falsy
177
+ * field: `?multipleStatements=true` would beat `false`, `?flags=LOCAL_FILES` would re-enable LOCAL
178
+ * INFILE). Pool of at most two connections, session reset on release; MySQL without
179
+ * `LOCAL_FILES` and multiple statements.
180
+ */
181
+ export declare function consoleConnectionOptions(dialect: ConsoleCredentialDialect, main: Readonly<Record<string, unknown>>, credential: ConsoleCredential): Record<string, unknown>;