pgsql-client 0.0.1 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,23 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2025 Dan Lynch <pyramation@gmail.com>
4
+ Copyright (c) 2025 Constructive <developers@constructive.io>
5
+ Copyright (c) 2020-present, Interweb, Inc.
6
+
7
+ Permission is hereby granted, free of charge, to any person obtaining a copy
8
+ of this software and associated documentation files (the "Software"), to deal
9
+ in the Software without restriction, including without limitation the rights
10
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11
+ copies of the Software, and to permit persons to whom the Software is
12
+ furnished to do so, subject to the following conditions:
13
+
14
+ The above copyright notice and this permission notice shall be included in all
15
+ copies or substantial portions of the Software.
16
+
17
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23
+ SOFTWARE.
package/README.md CHANGED
@@ -8,13 +8,25 @@
8
8
  <a href="https://github.com/constructive-io/constructive/actions/workflows/run-tests.yaml">
9
9
  <img height="20" src="https://github.com/constructive-io/constructive/actions/workflows/run-tests.yaml/badge.svg" />
10
10
  </a>
11
- <a href="https://github.com/constructive-io/constructive/blob/main/LICENSE"><img height="20" src="https://img.shields.io/badge/license-MIT-blue.svg"/></a>
12
- <a href="https://www.npmjs.com/package/pgsql-client"><img height="20" src="https://img.shields.io/github/package-json/v/constructive-io/constructive?filename=postgres%2Fpgsql-client%2Fpackage.json"/></a>
11
+ <a href="https://github.com/constructive-io/constructive/blob/main/LICENSE">
12
+ <img height="20" src="https://img.shields.io/badge/license-MIT-blue.svg"/>
13
+ </a>
14
+ <a href="https://www.npmjs.com/package/pgsql-client">
15
+ <img height="20" src="https://img.shields.io/github/package-json/v/constructive-io/constructive?filename=postgres%2Fpgsql-client%2Fpackage.json"/>
16
+ </a>
13
17
  </p>
14
18
 
15
- # pgsql-client
19
+ PostgreSQL client utilities with query helpers, RLS context management, and database administration.
20
+
21
+ ## Overview
16
22
 
17
- A small utility for executing PostgreSQL queries within a session context using [`pg`](https://www.npmjs.com/package/pg). It allows you to temporarily set PostgreSQL session variables (`set_config`) for RLS (Row-Level Security) and other scoped operations.
23
+ `pgsql-client` provides a set of utilities for working with PostgreSQL databases:
24
+
25
+ - **DbAdmin**: Database administration operations (create, drop, templates, extensions, grants)
26
+ - **PgClient**: Query helpers with RLS context management
27
+ - **Role utilities**: Role name mapping for anonymous, authenticated, and administrator roles
28
+ - **Context utilities**: Generate SQL for setting PostgreSQL session context variables
29
+ - **Stream utilities**: Stream SQL to psql process
18
30
 
19
31
  ## Installation
20
32
 
@@ -22,71 +34,99 @@ A small utility for executing PostgreSQL queries within a session context using
22
34
  npm install pgsql-client
23
35
  ```
24
36
 
25
- ## Features
26
-
27
- * Sets session-level context (e.g., role, user ID) using `set_config`.
28
- * Automatically wraps execution in a transaction (`BEGIN`/`COMMIT`).
29
- * Automatically rolls back on error.
30
- * Supports both `Pool` and `Client` from `pg`.
31
-
32
37
  ## Usage
33
38
 
34
- ```ts
35
- import pgQueryContext from 'pgsql-client';
36
- import { Pool } from 'pg';
39
+ ### DbAdmin
37
40
 
38
- const pool = new Pool();
41
+ ```typescript
42
+ import { DbAdmin } from 'pgsql-client';
39
43
 
40
- const result = await pgQueryContext({
41
- client: pool,
42
- context: {
43
- 'role': 'authenticated',
44
- 'myapp.user_id': '123e4567-e89b-12d3-a456-426614174000'
45
- },
46
- query: 'SELECT * FROM app_private.do_something_secure($1)',
47
- variables: ['input-value']
44
+ const admin = new DbAdmin({
45
+ host: 'localhost',
46
+ port: 5432,
47
+ user: 'postgres',
48
+ password: 'password',
49
+ database: 'mydb'
48
50
  });
49
51
 
50
- console.log(result.rows);
51
- ```
52
+ // Create a database
53
+ admin.create('mydb');
52
54
 
53
- ## API
55
+ // Install extensions
56
+ admin.installExtensions(['uuid-ossp', 'pgcrypto'], 'mydb');
54
57
 
55
- ### `pgQueryContext(options: ExecOptions): Promise<QueryResult>`
58
+ // Drop a database
59
+ admin.drop('mydb');
60
+ ```
56
61
 
57
- #### Options
62
+ ### PgClient
58
63
 
59
- | Name | Type | Required | Description |
60
- | ----------- | ------------------------ | -------- | ------------------------------------------------------ |
61
- | `client` | `Pool` or `ClientBase` | ✅ | The PostgreSQL client or pool to use |
62
- | `context` | `Record<string, string>` | ❌ | Key-value session variables to be set via `set_config` |
63
- | `query` | `string` | ✅ | SQL query to run |
64
- | `variables` | `any[]` | ❌ | Parameterized query variables |
64
+ ```typescript
65
+ import { PgClient } from 'pgsql-client';
65
66
 
66
- ## Example with `express`
67
+ const client = new PgClient({
68
+ host: 'localhost',
69
+ port: 5432,
70
+ user: 'app_user',
71
+ password: 'password',
72
+ database: 'mydb'
73
+ });
67
74
 
68
- ```ts
69
- app.post('/secure-endpoint', async (req, res) => {
70
- const authToken = req.headers.authorization;
75
+ // Query helpers
76
+ const users = await client.any('SELECT * FROM users');
77
+ const user = await client.one('SELECT * FROM users WHERE id = $1', [userId]);
78
+ const maybeUser = await client.oneOrNone('SELECT * FROM users WHERE email = $1', [email]);
71
79
 
72
- const result = await pgQueryContext({
73
- client: pool,
74
- context: {
75
- 'role': 'authenticated',
76
- 'myapp.token': authToken,
77
- },
78
- query: 'SELECT * FROM app_private.verify_token($1)',
79
- variables: [authToken],
80
- });
80
+ // Set RLS context
81
+ client.setContext({ role: 'authenticated', 'jwt.claims.user_id': userId });
81
82
 
82
- if (!result.rows.length) {
83
- return res.status(401).json({ error: 'Unauthorized' });
84
- }
83
+ // Or use the auth helper
84
+ client.auth({ role: 'authenticated', userId: userId });
85
85
 
86
- res.json({ user: result.rows[0] });
87
- });
86
+ // Close the connection
87
+ await client.close();
88
88
  ```
89
89
 
90
+ ## API
91
+
92
+ ### DbAdmin
93
+
94
+ - `create(dbName?)` - Create a database
95
+ - `drop(dbName?)` - Drop a database
96
+ - `createFromTemplate(template, dbName?)` - Create database from template
97
+ - `installExtensions(extensions, dbName?)` - Install PostgreSQL extensions
98
+ - `connectionString(dbName?)` - Generate connection string
99
+ - `createTemplateFromBase(base, template)` - Create template database
100
+ - `cleanupTemplate(template)` - Clean up template database
101
+ - `grantRole(role, user, dbName?)` - Grant role to user
102
+ - `grantConnect(role, dbName?)` - Grant connect privilege
103
+ - `createUserRole(user, password, dbName)` - Create user with roles
104
+ - `loadSql(file, dbName)` - Load SQL file
105
+ - `streamSql(sql, dbName)` - Stream SQL to database
106
+
107
+ ### PgClient
108
+
109
+ - `query(sql, values?)` - Execute query with context
110
+ - `any(sql, values?)` - Return all rows
111
+ - `one(sql, values?)` - Return exactly one row (throws if not exactly one)
112
+ - `oneOrNone(sql, values?)` - Return one row or null
113
+ - `many(sql, values?)` - Return many rows (throws if none)
114
+ - `manyOrNone(sql, values?)` - Return rows or empty array
115
+ - `none(sql, values?)` - Execute without returning rows
116
+ - `result(sql, values?)` - Return full QueryResult
117
+ - `begin()` - Begin transaction
118
+ - `commit()` - Commit transaction
119
+ - `savepoint(name?)` - Create savepoint
120
+ - `rollback(name?)` - Rollback to savepoint
121
+ - `setContext(ctx)` - Set session context variables
122
+ - `auth(options?)` - Set authentication context
123
+ - `clearContext()` - Clear context and reset to anonymous
124
+ - `close()` - Close connection
125
+
126
+ ## License
127
+
128
+ MIT
129
+
90
130
  ---
91
131
 
92
132
  ## Education and Tutorials
@@ -114,12 +154,17 @@ Common issues and solutions for pgpm, PostgreSQL, and testing.
114
154
 
115
155
  ## Related Constructive Tooling
116
156
 
157
+ ### 📦 Package Management
158
+
159
+ * [pgpm](https://github.com/constructive-io/constructive/tree/main/pgpm/pgpm): **🖥️ PostgreSQL Package Manager** for modular Postgres development. Works with database workspaces, scaffolding, migrations, seeding, and installing database packages.
160
+
117
161
  ### 🧪 Testing
118
162
 
119
163
  * [pgsql-test](https://github.com/constructive-io/constructive/tree/main/postgres/pgsql-test): **📊 Isolated testing environments** with per-test transaction rollbacks—ideal for integration tests, complex migrations, and RLS simulation.
164
+ * [pgsql-seed](https://github.com/constructive-io/constructive/tree/main/postgres/pgsql-seed): **🌱 PostgreSQL seeding utilities** for CSV, JSON, SQL data loading, and pgpm deployment.
120
165
  * [supabase-test](https://github.com/constructive-io/constructive/tree/main/postgres/supabase-test): **🧪 Supabase-native test harness** preconfigured for the local Supabase stack—per-test rollbacks, JWT/role context helpers, and CI/GitHub Actions ready.
121
166
  * [graphile-test](https://github.com/constructive-io/constructive/tree/main/graphile/graphile-test): **🔐 Authentication mocking** for Graphile-focused test helpers and emulating row-level security contexts.
122
- * [pgsql-client](https://github.com/constructive-io/constructive/tree/main/postgres/pgsql-client): **🔒 Session context injection** to add session-local context (e.g., `SET LOCAL`) into queries—ideal for setting `role`, `jwt.claims`, and other session settings.
167
+ * [pg-query-context](https://github.com/constructive-io/constructive/tree/main/postgres/pg-query-context): **🔒 Session context injection** to add session-local context (e.g., `SET LOCAL`) into queries—ideal for setting `role`, `jwt.claims`, and other session settings.
123
168
 
124
169
  ### 🧠 Parsing & AST
125
170
 
@@ -130,28 +175,6 @@ Common issues and solutions for pgpm, PostgreSQL, and testing.
130
175
  * [@pgsql/types](https://www.npmjs.com/package/@pgsql/types): **📝 Type definitions** for PostgreSQL AST nodes in TypeScript.
131
176
  * [@pgsql/utils](https://www.npmjs.com/package/@pgsql/utils): **🛠️ AST utilities** for constructing and transforming PostgreSQL syntax trees.
132
177
 
133
- ### 🚀 API & Dev Tools
134
-
135
- * [@constructive-io/graphql-server](https://github.com/constructive-io/constructive/tree/main/graphql/server): **⚡ Express-based API server** powered by PostGraphile to expose a secure, scalable GraphQL API over your Postgres database.
136
- * [@constructive-io/graphql-explorer](https://github.com/constructive-io/constructive/tree/main/graphql/explorer): **🔎 Visual API explorer** with GraphiQL for browsing across all databases and schemas—useful for debugging, documentation, and API prototyping.
137
-
138
- ### 🔁 Streaming & Uploads
139
-
140
- * [etag-hash](https://github.com/constructive-io/constructive/tree/main/streaming/etag-hash): **🏷️ S3-compatible ETags** created by streaming and hashing file uploads in chunks.
141
- * [etag-stream](https://github.com/constructive-io/constructive/tree/main/streaming/etag-stream): **🔄 ETag computation** via Node stream transformer during upload or transfer.
142
- * [uuid-hash](https://github.com/constructive-io/constructive/tree/main/streaming/uuid-hash): **🆔 Deterministic UUIDs** generated from hashed content, great for deduplication and asset referencing.
143
- * [uuid-stream](https://github.com/constructive-io/constructive/tree/main/streaming/uuid-stream): **🌊 Streaming UUID generation** based on piped file content—ideal for upload pipelines.
144
- * [@constructive-io/s3-streamer](https://github.com/constructive-io/constructive/tree/main/streaming/s3-streamer): **📤 Direct S3 streaming** for large files with support for metadata injection and content validation.
145
- * [@constructive-io/upload-names](https://github.com/constructive-io/constructive/tree/main/streaming/upload-names): **📂 Collision-resistant filenames** utility for structured and unique file names for uploads.
146
-
147
- ### 🧰 CLI & Codegen
148
-
149
- * [pgpm](https://github.com/constructive-io/constructive/tree/main/pgpm/pgpm): **🖥️ PostgreSQL Package Manager** for modular Postgres development. Works with database workspaces, scaffolding, migrations, seeding, and installing database packages.
150
- * [@constructive-io/cli](https://github.com/constructive-io/constructive/tree/main/packages/cli): **🖥️ Command-line toolkit** for managing Constructive projects—supports database scaffolding, migrations, seeding, code generation, and automation.
151
- * [@constructive-io/graphql-codegen](https://github.com/constructive-io/constructive/tree/main/graphql/codegen): **✨ GraphQL code generation** (types, operations, SDK) from schema/endpoint introspection.
152
- * [@constructive-io/query-builder](https://github.com/constructive-io/constructive/tree/main/packages/query-builder): **🏗️ SQL constructor** providing a robust TypeScript-based query builder for dynamic generation of `SELECT`, `INSERT`, `UPDATE`, `DELETE`, and stored procedure calls—supports advanced SQL features like `JOIN`, `GROUP BY`, and schema-qualified queries.
153
- * [@constructive-io/graphql-query](https://github.com/constructive-io/constructive/tree/main/graphql/query): **🧩 Fluent GraphQL builder** for PostGraphile schemas. ⚡ Schema-aware via introspection, 🧩 composable and ergonomic for building deeply nested queries.
154
-
155
178
  ## Credits
156
179
 
157
180
  **🛠 Built by the [Constructive](https://constructive.io) team — creators of modular Postgres tooling for secure, composable backends. If you like our work, contribute on [GitHub](https://github.com/constructive-io).**
package/admin.d.ts ADDED
@@ -0,0 +1,24 @@
1
+ import { PgTestConnectionOptions } from '@pgpmjs/types';
2
+ import { PgConfig } from 'pg-env';
3
+ export declare class DbAdmin {
4
+ protected config: PgConfig;
5
+ protected verbose: boolean;
6
+ protected roleConfig?: PgTestConnectionOptions;
7
+ constructor(config: PgConfig, verbose?: boolean, roleConfig?: PgTestConnectionOptions);
8
+ private getEnv;
9
+ private run;
10
+ private safeDropDb;
11
+ drop(dbName?: string): void;
12
+ dropTemplate(dbName: string): void;
13
+ create(dbName?: string): void;
14
+ createFromTemplate(template: string, dbName?: string): void;
15
+ installExtensions(extensions: string[] | string, dbName?: string): void;
16
+ connectionString(dbName?: string): string;
17
+ createTemplateFromBase(base: string, template: string): void;
18
+ cleanupTemplate(template: string): void;
19
+ grantRole(role: string, user: string, dbName?: string): Promise<void>;
20
+ grantConnect(role: string, dbName?: string): Promise<void>;
21
+ createUserRole(user: string, password: string, dbName: string, useLocksForRoles?: boolean): Promise<void>;
22
+ loadSql(file: string, dbName: string): void;
23
+ streamSql(sql: string, dbName: string): Promise<void>;
24
+ }
package/admin.js ADDED
@@ -0,0 +1,130 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DbAdmin = void 0;
4
+ const core_1 = require("@pgpmjs/core");
5
+ const logger_1 = require("@pgpmjs/logger");
6
+ const child_process_1 = require("child_process");
7
+ const fs_1 = require("fs");
8
+ const pg_env_1 = require("pg-env");
9
+ const roles_1 = require("./roles");
10
+ const stream_1 = require("./stream");
11
+ const log = new logger_1.Logger('db-admin');
12
+ class DbAdmin {
13
+ config;
14
+ verbose;
15
+ roleConfig;
16
+ constructor(config, verbose = false, roleConfig) {
17
+ this.config = (0, pg_env_1.getPgEnvOptions)(config);
18
+ this.verbose = verbose;
19
+ this.roleConfig = roleConfig;
20
+ }
21
+ getEnv() {
22
+ return {
23
+ PGHOST: this.config.host,
24
+ PGPORT: String(this.config.port),
25
+ PGUSER: this.config.user,
26
+ PGPASSWORD: this.config.password
27
+ };
28
+ }
29
+ run(command) {
30
+ try {
31
+ (0, child_process_1.execSync)(command, {
32
+ stdio: this.verbose ? 'inherit' : 'pipe',
33
+ env: {
34
+ ...process.env,
35
+ ...this.getEnv()
36
+ }
37
+ });
38
+ if (this.verbose)
39
+ log.success(`Executed: ${command}`);
40
+ }
41
+ catch (err) {
42
+ log.error(`Command failed: ${command}`);
43
+ if (this.verbose)
44
+ log.error(err.message);
45
+ throw err;
46
+ }
47
+ }
48
+ safeDropDb(name) {
49
+ try {
50
+ this.run(`dropdb "${name}"`);
51
+ }
52
+ catch (err) {
53
+ if (!err.message.includes('does not exist')) {
54
+ log.warn(`Could not drop database ${name}: ${err.message}`);
55
+ }
56
+ }
57
+ }
58
+ drop(dbName) {
59
+ this.safeDropDb(dbName ?? this.config.database);
60
+ }
61
+ dropTemplate(dbName) {
62
+ this.run(`psql -c "UPDATE pg_database SET datistemplate='false' WHERE datname='${dbName}'"`);
63
+ this.drop(dbName);
64
+ }
65
+ create(dbName) {
66
+ const db = dbName ?? this.config.database;
67
+ this.run(`createdb -U ${this.config.user} -h ${this.config.host} -p ${this.config.port} "${db}"`);
68
+ }
69
+ createFromTemplate(template, dbName) {
70
+ const db = dbName ?? this.config.database;
71
+ this.run(`createdb -U ${this.config.user} -h ${this.config.host} -p ${this.config.port} -e "${db}" -T "${template}"`);
72
+ }
73
+ installExtensions(extensions, dbName) {
74
+ const db = dbName ?? this.config.database;
75
+ const extList = typeof extensions === 'string' ? extensions.split(',') : extensions;
76
+ for (const extension of extList) {
77
+ this.run(`psql --dbname "${db}" -c 'CREATE EXTENSION IF NOT EXISTS "${extension}" CASCADE;'`);
78
+ }
79
+ }
80
+ connectionString(dbName) {
81
+ const { user, password, host, port } = this.config;
82
+ const db = dbName ?? this.config.database;
83
+ return `postgres://${user}:${password}@${host}:${port}/${db}`;
84
+ }
85
+ createTemplateFromBase(base, template) {
86
+ this.run(`createdb -T "${base}" "${template}"`);
87
+ this.run(`psql -c "UPDATE pg_database SET datistemplate = true WHERE datname = '${template}';"`);
88
+ }
89
+ cleanupTemplate(template) {
90
+ try {
91
+ this.run(`psql -c "UPDATE pg_database SET datistemplate = false WHERE datname = '${template}'"`);
92
+ }
93
+ catch {
94
+ log.warn(`Skipping failed UPDATE of datistemplate for ${template}`);
95
+ }
96
+ this.safeDropDb(template);
97
+ }
98
+ async grantRole(role, user, dbName) {
99
+ const db = dbName ?? this.config.database;
100
+ const sql = (0, core_1.generateGrantRoleSQL)(role, user);
101
+ await this.streamSql(sql, db);
102
+ }
103
+ async grantConnect(role, dbName) {
104
+ const db = dbName ?? this.config.database;
105
+ const sql = `GRANT CONNECT ON DATABASE "${db}" TO ${role};`;
106
+ await this.streamSql(sql, db);
107
+ }
108
+ // ONLY granting admin role for testing purposes, normally the db connection for apps won't have admin role
109
+ // DO NOT USE THIS FOR PRODUCTION
110
+ async createUserRole(user, password, dbName, useLocksForRoles = false) {
111
+ const anonRole = (0, roles_1.getRoleName)('anonymous', this.roleConfig);
112
+ const authRole = (0, roles_1.getRoleName)('authenticated', this.roleConfig);
113
+ const adminRole = (0, roles_1.getRoleName)('administrator', this.roleConfig);
114
+ const sql = (0, core_1.generateCreateUserWithGrantsSQL)(user, password, [anonRole, authRole, adminRole], useLocksForRoles);
115
+ await this.streamSql(sql, dbName);
116
+ }
117
+ loadSql(file, dbName) {
118
+ if (!(0, fs_1.existsSync)(file)) {
119
+ throw new Error(`Missing SQL file: ${file}`);
120
+ }
121
+ this.run(`psql -f ${file} ${dbName}`);
122
+ }
123
+ async streamSql(sql, dbName) {
124
+ await (0, stream_1.streamSql)({
125
+ ...this.config,
126
+ database: dbName
127
+ }, sql);
128
+ }
129
+ }
130
+ exports.DbAdmin = DbAdmin;
package/client.d.ts ADDED
@@ -0,0 +1,42 @@
1
+ import { Client, QueryResult } from 'pg';
2
+ import { PgConfig } from 'pg-env';
3
+ import { AuthOptions, PgTestConnectionOptions, PgTestClientContext } from '@pgpmjs/types';
4
+ export type PgClientOpts = {
5
+ deferConnect?: boolean;
6
+ trackConnect?: (p: Promise<any>) => void;
7
+ } & Partial<PgTestConnectionOptions>;
8
+ export declare class PgClient {
9
+ config: PgConfig;
10
+ client: Client;
11
+ protected opts: PgClientOpts;
12
+ protected ctxStmts: string;
13
+ protected contextSettings: PgTestClientContext;
14
+ protected _ended: boolean;
15
+ protected connectPromise: Promise<void> | null;
16
+ constructor(config: PgConfig, opts?: PgClientOpts);
17
+ protected ensureConnected(): Promise<void>;
18
+ close(): Promise<void>;
19
+ begin(): Promise<void>;
20
+ savepoint(name?: string): Promise<void>;
21
+ rollback(name?: string): Promise<void>;
22
+ commit(): Promise<void>;
23
+ setContext(ctx: Record<string, string | null>): void;
24
+ /**
25
+ * Set authentication context for the current session.
26
+ * Configures role and user ID using cascading defaults from options -> opts.auth -> RoleMapping.
27
+ */
28
+ auth(options?: AuthOptions): void;
29
+ /**
30
+ * Clear all session context variables and reset to default anonymous role.
31
+ */
32
+ clearContext(): void;
33
+ any<T = any>(query: string, values?: any[]): Promise<T[]>;
34
+ one<T = any>(query: string, values?: any[]): Promise<T>;
35
+ oneOrNone<T = any>(query: string, values?: any[]): Promise<T | null>;
36
+ many<T = any>(query: string, values?: any[]): Promise<T[]>;
37
+ manyOrNone<T = any>(query: string, values?: any[]): Promise<T[]>;
38
+ none(query: string, values?: any[]): Promise<void>;
39
+ result(query: string, values?: any[]): Promise<QueryResult>;
40
+ ctxQuery(): Promise<void>;
41
+ query<T = any>(query: string, values?: any[]): Promise<QueryResult<T>>;
42
+ }
package/client.js ADDED
@@ -0,0 +1,130 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PgClient = void 0;
4
+ const pg_1 = require("pg");
5
+ const roles_1 = require("./roles");
6
+ const context_utils_1 = require("./context-utils");
7
+ class PgClient {
8
+ config;
9
+ client;
10
+ opts;
11
+ ctxStmts = '';
12
+ contextSettings = {};
13
+ _ended = false;
14
+ connectPromise = null;
15
+ constructor(config, opts = {}) {
16
+ this.opts = opts;
17
+ this.config = config;
18
+ this.client = new pg_1.Client({
19
+ host: this.config.host,
20
+ port: this.config.port,
21
+ database: this.config.database,
22
+ user: this.config.user,
23
+ password: this.config.password
24
+ });
25
+ if (!opts.deferConnect) {
26
+ this.connectPromise = this.client.connect();
27
+ if (opts.trackConnect)
28
+ opts.trackConnect(this.connectPromise);
29
+ }
30
+ }
31
+ async ensureConnected() {
32
+ if (this.connectPromise) {
33
+ try {
34
+ await this.connectPromise;
35
+ }
36
+ catch { }
37
+ }
38
+ }
39
+ async close() {
40
+ if (!this._ended) {
41
+ this._ended = true;
42
+ await this.ensureConnected();
43
+ await this.client.end();
44
+ }
45
+ }
46
+ async begin() {
47
+ await this.client.query('BEGIN;');
48
+ }
49
+ async savepoint(name = 'lqlsavepoint') {
50
+ await this.client.query(`SAVEPOINT "${name}";`);
51
+ }
52
+ async rollback(name = 'lqlsavepoint') {
53
+ await this.client.query(`ROLLBACK TO SAVEPOINT "${name}";`);
54
+ }
55
+ async commit() {
56
+ await this.client.query('COMMIT;');
57
+ }
58
+ setContext(ctx) {
59
+ Object.assign(this.contextSettings, ctx);
60
+ this.ctxStmts = (0, context_utils_1.generateContextStatements)(this.contextSettings);
61
+ }
62
+ /**
63
+ * Set authentication context for the current session.
64
+ * Configures role and user ID using cascading defaults from options -> opts.auth -> RoleMapping.
65
+ */
66
+ auth(options = {}) {
67
+ const role = options.role ?? this.opts.auth?.role ?? (0, roles_1.getRoleName)('authenticated', this.opts);
68
+ const userIdKey = options.userIdKey ?? this.opts.auth?.userIdKey ?? 'jwt.claims.user_id';
69
+ const userId = options.userId ?? this.opts.auth?.userId ?? null;
70
+ this.setContext({
71
+ role,
72
+ [userIdKey]: userId !== null ? String(userId) : null
73
+ });
74
+ }
75
+ /**
76
+ * Clear all session context variables and reset to default anonymous role.
77
+ */
78
+ clearContext() {
79
+ const defaultRole = (0, roles_1.getRoleName)('anonymous', this.opts);
80
+ const nulledSettings = {};
81
+ Object.keys(this.contextSettings).forEach(key => {
82
+ nulledSettings[key] = null;
83
+ });
84
+ nulledSettings.role = defaultRole;
85
+ this.ctxStmts = (0, context_utils_1.generateContextStatements)(nulledSettings);
86
+ this.contextSettings = { role: defaultRole };
87
+ }
88
+ async any(query, values) {
89
+ const result = await this.query(query, values);
90
+ return result.rows;
91
+ }
92
+ async one(query, values) {
93
+ const rows = await this.any(query, values);
94
+ if (rows.length !== 1) {
95
+ throw new Error('Expected exactly one result');
96
+ }
97
+ return rows[0];
98
+ }
99
+ async oneOrNone(query, values) {
100
+ const rows = await this.any(query, values);
101
+ return rows[0] || null;
102
+ }
103
+ async many(query, values) {
104
+ const rows = await this.any(query, values);
105
+ if (rows.length === 0)
106
+ throw new Error('Expected many rows, got none');
107
+ return rows;
108
+ }
109
+ async manyOrNone(query, values) {
110
+ return this.any(query, values);
111
+ }
112
+ async none(query, values) {
113
+ await this.query(query, values);
114
+ }
115
+ async result(query, values) {
116
+ return this.query(query, values);
117
+ }
118
+ async ctxQuery() {
119
+ if (this.ctxStmts) {
120
+ await this.client.query(this.ctxStmts);
121
+ }
122
+ }
123
+ // NOTE: all queries should call ctxQuery() before executing the query
124
+ async query(query, values) {
125
+ await this.ctxQuery();
126
+ const result = await this.client.query(query, values);
127
+ return result;
128
+ }
129
+ }
130
+ exports.PgClient = PgClient;
@@ -0,0 +1,8 @@
1
+ import type { PgTestClientContext } from '@pgpmjs/types';
2
+ /**
3
+ * Generate SQL statements to set PostgreSQL session context variables
4
+ * Uses SET LOCAL ROLE for the 'role' key and set_config() for other variables
5
+ * @param context - Context settings to apply
6
+ * @returns SQL string with SET LOCAL ROLE and set_config() statements
7
+ */
8
+ export declare function generateContextStatements(context: PgTestClientContext): string;
@@ -0,0 +1,28 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.generateContextStatements = generateContextStatements;
4
+ /**
5
+ * Generate SQL statements to set PostgreSQL session context variables
6
+ * Uses SET LOCAL ROLE for the 'role' key and set_config() for other variables
7
+ * @param context - Context settings to apply
8
+ * @returns SQL string with SET LOCAL ROLE and set_config() statements
9
+ */
10
+ function generateContextStatements(context) {
11
+ return Object.entries(context)
12
+ .map(([key, val]) => {
13
+ if (key === 'role') {
14
+ if (val === null || val === undefined) {
15
+ return 'SET LOCAL ROLE NONE;';
16
+ }
17
+ const escapedRole = val.replace(/"/g, '""');
18
+ return `SET LOCAL ROLE "${escapedRole}";`;
19
+ }
20
+ // Use set_config for other context variables
21
+ if (val === null || val === undefined) {
22
+ return `SELECT set_config('${key}', NULL, true);`;
23
+ }
24
+ const escapedVal = val.replace(/'/g, "''");
25
+ return `SELECT set_config('${key}', '${escapedVal}', true);`;
26
+ })
27
+ .join('\n');
28
+ }