@zhin.js/database 1.1.8 → 1.2.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.
@@ -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>;