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 +23 -0
- package/README.md +98 -75
- package/admin.d.ts +24 -0
- package/admin.js +130 -0
- package/client.d.ts +42 -0
- package/client.js +130 -0
- package/context-utils.d.ts +8 -0
- package/context-utils.js +28 -0
- package/esm/admin.js +126 -0
- package/esm/client.js +126 -0
- package/esm/context-utils.js +25 -0
- package/esm/index.js +5 -36
- package/esm/roles.js +32 -0
- package/esm/stream.js +96 -0
- package/index.d.ts +5 -9
- package/index.js +20 -35
- package/package.json +30 -11
- package/roles.d.ts +17 -0
- package/roles.js +38 -0
- package/stream.d.ts +33 -0
- package/stream.js +99 -0
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
|
-
|
|
12
|
-
|
|
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
|
-
|
|
19
|
+
PostgreSQL client utilities with query helpers, RLS context management, and database administration.
|
|
20
|
+
|
|
21
|
+
## Overview
|
|
16
22
|
|
|
17
|
-
|
|
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
|
-
|
|
35
|
-
import pgQueryContext from 'pgsql-client';
|
|
36
|
-
import { Pool } from 'pg';
|
|
39
|
+
### DbAdmin
|
|
37
40
|
|
|
38
|
-
|
|
41
|
+
```typescript
|
|
42
|
+
import { DbAdmin } from 'pgsql-client';
|
|
39
43
|
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
51
|
-
|
|
52
|
+
// Create a database
|
|
53
|
+
admin.create('mydb');
|
|
52
54
|
|
|
53
|
-
|
|
55
|
+
// Install extensions
|
|
56
|
+
admin.installExtensions(['uuid-ossp', 'pgcrypto'], 'mydb');
|
|
54
57
|
|
|
55
|
-
|
|
58
|
+
// Drop a database
|
|
59
|
+
admin.drop('mydb');
|
|
60
|
+
```
|
|
56
61
|
|
|
57
|
-
|
|
62
|
+
### PgClient
|
|
58
63
|
|
|
59
|
-
|
|
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
|
-
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
73
|
-
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
}
|
|
83
|
+
// Or use the auth helper
|
|
84
|
+
client.auth({ role: 'authenticated', userId: userId });
|
|
85
85
|
|
|
86
|
-
|
|
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
|
-
* [
|
|
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;
|
package/context-utils.js
ADDED
|
@@ -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
|
+
}
|