pgsql-test 0.0.1 → 2.0.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 +21 -0
- package/README.md +232 -36
- package/admin.d.ts +24 -0
- package/admin.js +130 -0
- package/connect.d.ts +18 -0
- package/connect.js +95 -0
- package/esm/admin.js +126 -0
- package/esm/connect.js +90 -0
- package/{src/index.ts → esm/index.js} +1 -1
- package/esm/legacy-connect.js +25 -0
- package/esm/manager.js +117 -0
- package/esm/seed.js +35 -0
- package/esm/stream.js +43 -0
- package/esm/test-client.js +88 -0
- package/index.d.ts +2 -0
- package/index.js +18 -0
- package/legacy-connect.d.ts +11 -0
- package/legacy-connect.js +30 -0
- package/manager.d.ts +21 -0
- package/manager.js +124 -0
- package/package.json +9 -5
- package/seed.d.ts +22 -0
- package/seed.js +38 -0
- package/stream.d.ts +2 -0
- package/stream.js +46 -0
- package/test-client.d.ts +25 -0
- package/test-client.js +92 -0
- package/__tests__/postgres-test.connections.test.ts +0 -81
- package/__tests__/postgres-test.grants.test.ts +0 -53
- package/__tests__/postgres-test.records.test.ts +0 -66
- package/__tests__/postgres-test.template.test.ts +0 -52
- package/__tests__/postgres-test.test.ts +0 -36
- package/jest.config.js +0 -18
- package/sql/roles.sql +0 -48
- package/sql/test.sql +0 -36
- package/src/admin.ts +0 -135
- package/src/connect.ts +0 -42
- package/src/legacy-connect.ts +0 -34
- package/src/manager.ts +0 -142
- package/src/stream.ts +0 -61
- package/src/test-client.ts +0 -113
- package/src/utils.ts +0 -48
- package/test-utils/index.ts +0 -2
- package/tsconfig.esm.json +0 -9
- package/tsconfig.json +0 -9
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Dan Lynch <pyramation@gmail.com>
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,61 +1,257 @@
|
|
|
1
1
|
# pgsql-test
|
|
2
2
|
|
|
3
|
-
<p align="center">
|
|
4
|
-
<img src="https://
|
|
5
|
-
PostgreSQL Testing in TypeScript
|
|
3
|
+
<p align="center" width="100%">
|
|
4
|
+
<img height="250" src="https://github.com/user-attachments/assets/d0456af5-b6e9-422e-a45d-2574d5be490f" />
|
|
6
5
|
</p>
|
|
7
6
|
|
|
8
|
-
|
|
7
|
+
<p align="center" width="100%">
|
|
8
|
+
<a href="https://github.com/launchql/launchql-2.0/actions/workflows/run-tests.yaml">
|
|
9
|
+
<img height="20" src="https://github.com/launchql/launchql-2.0/actions/workflows/run-tests.yaml/badge.svg" />
|
|
10
|
+
</a>
|
|
11
|
+
<a href="https://github.com/launchql/launchql-2.0/blob/main/LICENSE-MIT">
|
|
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-test">
|
|
15
|
+
<img height="20" src="https://img.shields.io/github/package-json/v/launchql/launchql-2.0?filename=packages%2Fpgsql-test%2Fpackage.json"/>
|
|
16
|
+
</a>
|
|
17
|
+
</p>
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
`pgsql-test` provides an isolated PostgreSQL testing environment with per-test transaction rollback, ideal for integration tests involving SQL, roles, simulations, and complex migrations. With automatic rollbacks and isolated contexts, it eliminates test interference while delivering tight feedback loops for happier developers. We made database testing simple so you can focus on writing good tests instead of fighting your environment.
|
|
21
|
+
|
|
22
|
+
## Install
|
|
9
23
|
|
|
10
24
|
```sh
|
|
11
25
|
npm install pgsql-test
|
|
12
26
|
```
|
|
13
|
-
## Table of contents
|
|
14
27
|
|
|
15
|
-
|
|
16
|
-
- [Install](#install)
|
|
17
|
-
- [Table of contents](#table-of-contents)
|
|
18
|
-
- [Developing](#developing)
|
|
19
|
-
- [Credits](#credits)
|
|
28
|
+
## Features
|
|
20
29
|
|
|
21
|
-
|
|
30
|
+
* ⚡ Quick-start setup with `getConnections()`
|
|
31
|
+
* 🧹 Easy teardown and cleanup
|
|
32
|
+
* 🔄 Per-test isolation using transactions and savepoints
|
|
33
|
+
* 🛡️ Role-based context for RLS testing
|
|
34
|
+
* 🌱 Flexible seed support via `.sql` files and programmatic functions
|
|
35
|
+
* 🧪 Auto-generated test databases with `UUID` suffix
|
|
36
|
+
* 📦 Built for tools like `sqitch`, supporting full schema initialization workflows
|
|
37
|
+
* 🧰 Designed for `Jest`, `Mocha`, or any async test runner
|
|
22
38
|
|
|
23
|
-
When first cloning the repo:
|
|
24
39
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
40
|
+
## Table of Contents
|
|
41
|
+
|
|
42
|
+
1. [Install](#install)
|
|
43
|
+
2. [Features](#features)
|
|
44
|
+
3. [Quick Start](#quick-start)
|
|
45
|
+
4. [getConnections() Overview](#getconnections-overview)
|
|
46
|
+
5. [PgTestClient API Overview](#pgtestclient-api-overview)
|
|
47
|
+
6. [Usage Examples](#usage-examples)
|
|
48
|
+
* [Basic Setup](#basic-setup)
|
|
49
|
+
* [Role-Based Context](#role-based-context)
|
|
50
|
+
* [SQL File Seeding](#sql-file-seeding)
|
|
51
|
+
* [Programmatic Seeding](#programmatic-seeding)
|
|
52
|
+
* [Composed Seeding](#composed-seeding)
|
|
53
|
+
7. [Environment Overrides](#environment-overrides)
|
|
54
|
+
8. [Disclaimer](#disclaimer)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
## ✨ Quick Start
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
import { getConnections } from 'pgsql-test';
|
|
61
|
+
|
|
62
|
+
let db, teardown;
|
|
63
|
+
|
|
64
|
+
beforeAll(async () => {
|
|
65
|
+
({ db, teardown } = await getConnections());
|
|
66
|
+
await db.query(`SELECT 1`); // ✅ Ready to run queries
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
afterAll(() => teardown());
|
|
29
70
|
```
|
|
30
71
|
|
|
31
|
-
|
|
72
|
+
## `getConnections()` Overview
|
|
32
73
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
74
|
+
The `getConnections()` helper sets up a fresh PostgreSQL test database and returns a structured object with:
|
|
75
|
+
|
|
76
|
+
* `pg`: a `PgTestClient` connected as the root or superuser — useful for administrative setup or introspection
|
|
77
|
+
* `db`: a `PgTestClient` connected as the app-level user — used for running tests with RLS and granted permissions
|
|
78
|
+
* `admin`: a `DbAdmin` utility for managing database state, extensions, roles, and templates
|
|
79
|
+
* `teardown()`: a function that shuts down the test environment and database pool
|
|
80
|
+
* `manager`: a shared connection pool manager (`PgTestConnector`) behind both clients
|
|
81
|
+
|
|
82
|
+
Together, these allow fast, isolated, role-aware test environments with per-test rollback and full control over setup and teardown.
|
|
83
|
+
|
|
84
|
+
The `PgTestClient` returned by `getConnections()` is a fully-featured wrapper around `pg.Pool`. It provides:
|
|
85
|
+
|
|
86
|
+
* Automatic transaction and savepoint management for test isolation
|
|
87
|
+
* Easy switching of role-based contexts for RLS testing
|
|
88
|
+
* A clean, high-level API for integration testing PostgreSQL systems
|
|
89
|
+
|
|
90
|
+
## `PgTestClient` API Overview
|
|
91
|
+
|
|
92
|
+
The `PgTestClient` returned by `getConnections()` wraps a `pg.Client` and provides convenient helpers for query execution, test isolation, and context switching.
|
|
93
|
+
|
|
94
|
+
### Common Methods
|
|
95
|
+
|
|
96
|
+
* `query(sql, values?)` – Run a raw SQL query and get the `QueryResult`
|
|
97
|
+
* `beforeEach()` – Begins a transaction and sets a savepoint (called at the start of each test)
|
|
98
|
+
* `afterEach()` – Rolls back to the savepoint and commits the outer transaction (cleans up test state)
|
|
99
|
+
* `setContext({ key: value })` – Sets PostgreSQL config variables (like `role`) to simulate RLS contexts
|
|
100
|
+
* `any`, `one`, `oneOrNone`, `many`, `manyOrNone`, `none`, `result` – Typed query helpers for specific result expectations
|
|
101
|
+
|
|
102
|
+
These methods make it easier to build expressive and isolated integration tests with strong typing and error handling.
|
|
103
|
+
|
|
104
|
+
The `PgTestClient` returned by `getConnections()` is a fully-featured wrapper around `pg.Pool`. It provides:
|
|
105
|
+
|
|
106
|
+
* Automatic transaction and savepoint management for test isolation
|
|
107
|
+
* Easy switching of role-based contexts for RLS testing
|
|
108
|
+
* A clean, high-level API for integration testing PostgreSQL systems
|
|
109
|
+
|
|
110
|
+
## Usage Examples
|
|
111
|
+
|
|
112
|
+
### ⚡ Basic Setup
|
|
113
|
+
|
|
114
|
+
```ts
|
|
115
|
+
import { getConnections } from 'pgsql-test';
|
|
116
|
+
|
|
117
|
+
let db; // A fully wrapped PgTestClient using pg.Pool with savepoint-based rollback per test
|
|
118
|
+
let teardown;
|
|
119
|
+
|
|
120
|
+
beforeAll(async () => {
|
|
121
|
+
({ db, teardown } = await getConnections());
|
|
122
|
+
|
|
123
|
+
await db.query(`
|
|
124
|
+
CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT);
|
|
125
|
+
CREATE TABLE posts (id SERIAL PRIMARY KEY, user_id INT REFERENCES users(id), content TEXT);
|
|
126
|
+
|
|
127
|
+
INSERT INTO users (name) VALUES ('Alice'), ('Bob');
|
|
128
|
+
INSERT INTO posts (user_id, content) VALUES (1, 'Hello world!'), (2, 'Graphile is cool!');
|
|
129
|
+
`);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
afterAll(() => teardown());
|
|
133
|
+
|
|
134
|
+
beforeEach(() => db.beforeEach());
|
|
135
|
+
afterEach(() => db.afterEach());
|
|
136
|
+
|
|
137
|
+
test('user count starts at 2', async () => {
|
|
138
|
+
const res = await db.query('SELECT COUNT(*) FROM users');
|
|
139
|
+
expect(res.rows[0].count).toBe('2');
|
|
140
|
+
});
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### 🔐 Role-Based Context
|
|
144
|
+
|
|
145
|
+
```ts
|
|
146
|
+
describe('authenticated role', () => {
|
|
147
|
+
beforeEach(async () => {
|
|
148
|
+
db.setContext({ role: 'authenticated' });
|
|
149
|
+
await db.beforeEach();
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
afterEach(() => db.afterEach());
|
|
153
|
+
|
|
154
|
+
it('runs as authenticated', async () => {
|
|
155
|
+
const res = await db.query(`SELECT current_setting('role', true) AS role`);
|
|
156
|
+
expect(res.rows[0].role).toBe('authenticated');
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### 🔌 SQL File Seeding
|
|
162
|
+
|
|
163
|
+
Use `.sql` files to set up your database state before tests:
|
|
164
|
+
|
|
165
|
+
```ts
|
|
166
|
+
import path from 'path';
|
|
167
|
+
import { getConnections, seed } from 'pgsql-test';
|
|
168
|
+
|
|
169
|
+
const sql = (f: string) => path.join(__dirname, 'sql', f);
|
|
170
|
+
|
|
171
|
+
let db;
|
|
172
|
+
let teardown;
|
|
173
|
+
|
|
174
|
+
beforeAll(async () => {
|
|
175
|
+
({ db, teardown } = await getConnections({}, seed.sqlfile([
|
|
176
|
+
sql('schema.sql'),
|
|
177
|
+
sql('fixtures.sql')
|
|
178
|
+
])));
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
afterAll(async () => {
|
|
182
|
+
await teardown();
|
|
183
|
+
});
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### 🧠 Programmatic Seeding
|
|
187
|
+
|
|
188
|
+
Use JavaScript functions to insert seed data:
|
|
189
|
+
|
|
190
|
+
```ts
|
|
191
|
+
import { getConnections, seed } from 'pgsql-test';
|
|
192
|
+
|
|
193
|
+
let db;
|
|
194
|
+
let teardown;
|
|
195
|
+
|
|
196
|
+
beforeAll(async () => {
|
|
197
|
+
({ db, teardown } = await getConnections({}, seed.fn(async ({ pg }) => {
|
|
198
|
+
await pg.query(`
|
|
199
|
+
INSERT INTO users (name) VALUES ('Seeded User');
|
|
200
|
+
`);
|
|
201
|
+
})));
|
|
202
|
+
});
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### 🧬 Composed Seeding
|
|
206
|
+
|
|
207
|
+
Combine multiple seeders with `seed.compose()`:
|
|
208
|
+
|
|
209
|
+
```ts
|
|
210
|
+
import path from 'path';
|
|
211
|
+
import { getConnections, seed } from 'pgsql-test';
|
|
212
|
+
|
|
213
|
+
const sql = (f: string) => path.join(__dirname, 'sql', f);
|
|
214
|
+
|
|
215
|
+
let db;
|
|
216
|
+
let teardown;
|
|
217
|
+
|
|
218
|
+
beforeAll(async () => {
|
|
219
|
+
({ db, teardown } = await getConnections({}, seed.compose([
|
|
220
|
+
seed.sqlfile([
|
|
221
|
+
sql('schema.sql'),
|
|
222
|
+
sql('roles.sql')
|
|
223
|
+
]),
|
|
224
|
+
seed.fn(async ({ pg }) => {
|
|
225
|
+
await pg.query(`INSERT INTO users (name) VALUES ('Composed');`);
|
|
226
|
+
})
|
|
227
|
+
])));
|
|
228
|
+
});
|
|
37
229
|
```
|
|
38
230
|
|
|
39
|
-
|
|
231
|
+
---
|
|
40
232
|
|
|
41
|
-
|
|
233
|
+
These examples show how flexible `pgsql-test` is for composing repeatable and transactional test database environments.
|
|
42
234
|
|
|
43
|
-
| Category | Tools | Description |
|
|
44
|
-
|----------------------|------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------|
|
|
45
|
-
| **Chain Information** | [**Chain Registry**](https://github.com/hyperweb-io/chain-registry), [**Utils**](https://www.npmjs.com/package/@chain-registry/utils), [**Client**](https://www.npmjs.com/package/@chain-registry/client) | Everything from token symbols, logos, and IBC denominations for all assets you want to support in your application. |
|
|
46
|
-
| **Wallet Connectors**| [**Interchain Kit**](https://github.com/hyperweb-io/interchain-kit)<sup>beta</sup>, [**Cosmos Kit**](https://github.com/hyperweb.io/cosmos-kit) | Experience the convenience of connecting with a variety of web3 wallets through a single, streamlined interface. |
|
|
47
|
-
| **Signing Clients** | [**InterchainJS**](https://github.com/hyperweb-io/interchainjs)<sup>beta</sup>, [**CosmJS**](https://github.com/cosmos/cosmjs) | A single, universal signing interface for any network |
|
|
48
|
-
| **SDK Clients** | [**Telescope**](https://github.com/hyperweb.io/telescope) | Your Frontend Companion for Building with TypeScript with Cosmos SDK Modules. |
|
|
49
|
-
| **Starter Kits** | [**Create Interchain App**](https://github.com/hyperweb-io/create-interchain-app)<sup>beta</sup>, [**Create Cosmos App**](https://github.com/hyperweb.io/create-cosmos-app) | Set up a modern Interchain app by running one command. |
|
|
50
|
-
| **UI Kits** | [**Interchain UI**](https://github.com/hyperweb.io/interchain-ui) | The Interchain Design System, empowering developers with a flexible, easy-to-use UI kit. |
|
|
51
|
-
| **Testing Frameworks** | [**Starship**](https://github.com/hyperweb.io/starship) | Unified Testing and Development for the Interchain. |
|
|
52
|
-
| **TypeScript Smart Contracts** | [**Create Hyperweb App**](https://github.com/hyperweb-io/create-hyperweb-app) | Build and deploy full-stack blockchain applications with TypeScript |
|
|
53
|
-
| **CosmWasm Contracts** | [**CosmWasm TS Codegen**](https://github.com/CosmWasm/ts-codegen) | Convert your CosmWasm smart contracts into dev-friendly TypeScript classes. |
|
|
54
235
|
|
|
55
|
-
## Credits
|
|
56
236
|
|
|
57
|
-
|
|
237
|
+
## Environment Overrides
|
|
58
238
|
|
|
239
|
+
`pgsql-test` respects the following env vars for DB connectivity:
|
|
240
|
+
|
|
241
|
+
* `PGHOST`
|
|
242
|
+
* `PGPORT`
|
|
243
|
+
* `PGUSER`
|
|
244
|
+
* `PGPASSWORD`
|
|
245
|
+
|
|
246
|
+
Override them in your test runner or CI config:
|
|
247
|
+
|
|
248
|
+
```yaml
|
|
249
|
+
env:
|
|
250
|
+
PGHOST: localhost
|
|
251
|
+
PGPORT: 5432
|
|
252
|
+
PGUSER: postgres
|
|
253
|
+
PGPASSWORD: password
|
|
254
|
+
```
|
|
59
255
|
|
|
60
256
|
## Disclaimer
|
|
61
257
|
|
package/admin.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { PgConfig } from '@launchql/types';
|
|
2
|
+
import { SeedAdapter } from './seed';
|
|
3
|
+
export declare class DbAdmin {
|
|
4
|
+
private config;
|
|
5
|
+
private verbose;
|
|
6
|
+
constructor(config: PgConfig, verbose?: boolean);
|
|
7
|
+
private getEnv;
|
|
8
|
+
private run;
|
|
9
|
+
private safeDropDb;
|
|
10
|
+
drop(dbName?: string): void;
|
|
11
|
+
dropTemplate(dbName: string): void;
|
|
12
|
+
create(dbName?: string): void;
|
|
13
|
+
createFromTemplate(template: string, dbName?: string): void;
|
|
14
|
+
installExtensions(extensions: string[] | string, dbName?: string): void;
|
|
15
|
+
connectionString(dbName?: string): string;
|
|
16
|
+
createTemplateFromBase(base: string, template: string): void;
|
|
17
|
+
cleanupTemplate(template: string): void;
|
|
18
|
+
grantRole(role: string, user: string, dbName?: string): Promise<void>;
|
|
19
|
+
grantConnect(role: string, dbName?: string): Promise<void>;
|
|
20
|
+
createUserRole(user: string, password: string, dbName: string): Promise<void>;
|
|
21
|
+
loadSql(file: string, dbName: string): void;
|
|
22
|
+
streamSql(sql: string, dbName: string): Promise<void>;
|
|
23
|
+
createSeededTemplate(templateName: string, adapter: SeedAdapter): 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 child_process_1 = require("child_process");
|
|
5
|
+
const types_1 = require("@launchql/types");
|
|
6
|
+
const fs_1 = require("fs");
|
|
7
|
+
const stream_1 = require("./stream");
|
|
8
|
+
class DbAdmin {
|
|
9
|
+
config;
|
|
10
|
+
verbose;
|
|
11
|
+
constructor(config, verbose = false) {
|
|
12
|
+
this.config = config;
|
|
13
|
+
this.verbose = verbose;
|
|
14
|
+
this.config = (0, types_1.getPgEnvOptions)(config);
|
|
15
|
+
}
|
|
16
|
+
getEnv() {
|
|
17
|
+
return {
|
|
18
|
+
PGHOST: this.config.host,
|
|
19
|
+
PGPORT: String(this.config.port),
|
|
20
|
+
PGUSER: this.config.user,
|
|
21
|
+
PGPASSWORD: this.config.password,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
run(command) {
|
|
25
|
+
(0, child_process_1.execSync)(command, {
|
|
26
|
+
stdio: this.verbose ? 'inherit' : 'pipe',
|
|
27
|
+
env: {
|
|
28
|
+
...process.env,
|
|
29
|
+
...this.getEnv(),
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
safeDropDb(name) {
|
|
34
|
+
try {
|
|
35
|
+
this.run(`dropdb "${name}"`);
|
|
36
|
+
}
|
|
37
|
+
catch (err) {
|
|
38
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
39
|
+
if (!message.includes('does not exist')) {
|
|
40
|
+
console.warn(`⚠️ Could not drop database ${name}: ${message}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
drop(dbName) {
|
|
45
|
+
this.safeDropDb(dbName ?? this.config.database);
|
|
46
|
+
}
|
|
47
|
+
dropTemplate(dbName) {
|
|
48
|
+
this.run(`psql -c "UPDATE pg_database SET datistemplate='false' WHERE datname='${dbName}';"`);
|
|
49
|
+
this.drop(dbName);
|
|
50
|
+
}
|
|
51
|
+
create(dbName) {
|
|
52
|
+
const db = dbName ?? this.config.database;
|
|
53
|
+
this.run(`createdb -U ${this.config.user} -h ${this.config.host} -p ${this.config.port} "${db}"`);
|
|
54
|
+
}
|
|
55
|
+
createFromTemplate(template, dbName) {
|
|
56
|
+
const db = dbName ?? this.config.database;
|
|
57
|
+
this.run(`createdb -U ${this.config.user} -h ${this.config.host} -p ${this.config.port} -e "${db}" -T "${template}"`);
|
|
58
|
+
}
|
|
59
|
+
installExtensions(extensions, dbName) {
|
|
60
|
+
const db = dbName ?? this.config.database;
|
|
61
|
+
const extList = typeof extensions === 'string' ? extensions.split(',') : extensions;
|
|
62
|
+
for (const extension of extList) {
|
|
63
|
+
this.run(`psql --dbname "${db}" -c 'CREATE EXTENSION IF NOT EXISTS "${extension}" CASCADE;'`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
connectionString(dbName) {
|
|
67
|
+
const { user, password, host, port } = this.config;
|
|
68
|
+
const db = dbName ?? this.config.database;
|
|
69
|
+
return `postgres://${user}:${password}@${host}:${port}/${db}`;
|
|
70
|
+
}
|
|
71
|
+
createTemplateFromBase(base, template) {
|
|
72
|
+
this.run(`createdb -T "${base}" "${template}"`);
|
|
73
|
+
this.run(`psql -c "UPDATE pg_database SET datistemplate = true WHERE datname = '${template}';"`);
|
|
74
|
+
}
|
|
75
|
+
cleanupTemplate(template) {
|
|
76
|
+
try {
|
|
77
|
+
this.run(`psql -c "UPDATE pg_database SET datistemplate = false WHERE datname = '${template}'"`);
|
|
78
|
+
}
|
|
79
|
+
catch { }
|
|
80
|
+
this.safeDropDb(template);
|
|
81
|
+
}
|
|
82
|
+
async grantRole(role, user, dbName) {
|
|
83
|
+
const db = dbName ?? this.config.database;
|
|
84
|
+
const sql = `GRANT ${role} TO ${user};`;
|
|
85
|
+
await this.streamSql(sql, db);
|
|
86
|
+
}
|
|
87
|
+
async grantConnect(role, dbName) {
|
|
88
|
+
const db = dbName ?? this.config.database;
|
|
89
|
+
const sql = `GRANT CONNECT ON DATABASE "${db}" TO ${role};`;
|
|
90
|
+
await this.streamSql(sql, db);
|
|
91
|
+
}
|
|
92
|
+
async createUserRole(user, password, dbName) {
|
|
93
|
+
const sql = `
|
|
94
|
+
DO $$
|
|
95
|
+
BEGIN
|
|
96
|
+
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = '${user}') THEN
|
|
97
|
+
CREATE ROLE ${user} LOGIN PASSWORD '${password}';
|
|
98
|
+
GRANT anonymous TO ${user};
|
|
99
|
+
GRANT authenticated TO ${user};
|
|
100
|
+
END IF;
|
|
101
|
+
END $$;
|
|
102
|
+
`.trim();
|
|
103
|
+
this.streamSql(sql, dbName);
|
|
104
|
+
}
|
|
105
|
+
loadSql(file, dbName) {
|
|
106
|
+
if (!(0, fs_1.existsSync)(file)) {
|
|
107
|
+
throw new Error(`Missing SQL file: ${file}`);
|
|
108
|
+
}
|
|
109
|
+
this.run(`psql -f ${file} ${dbName}`);
|
|
110
|
+
}
|
|
111
|
+
async streamSql(sql, dbName) {
|
|
112
|
+
await (0, stream_1.streamSql)({
|
|
113
|
+
...this.config,
|
|
114
|
+
database: dbName
|
|
115
|
+
}, sql);
|
|
116
|
+
}
|
|
117
|
+
async createSeededTemplate(templateName, adapter) {
|
|
118
|
+
const seedDb = this.config.database;
|
|
119
|
+
this.create(seedDb);
|
|
120
|
+
await adapter.seed({
|
|
121
|
+
admin: this,
|
|
122
|
+
config: this.config,
|
|
123
|
+
pg: null // sorry!
|
|
124
|
+
});
|
|
125
|
+
this.cleanupTemplate(templateName);
|
|
126
|
+
this.createTemplateFromBase(seedDb, templateName);
|
|
127
|
+
this.drop(seedDb);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
exports.DbAdmin = DbAdmin;
|
package/connect.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { DbAdmin } from './admin';
|
|
2
|
+
import { TestConnectionOptions, PgConfig } from '@launchql/types';
|
|
3
|
+
import { PgTestConnector } from './manager';
|
|
4
|
+
import { SeedAdapter } from './seed';
|
|
5
|
+
import { PgTestClient } from './test-client';
|
|
6
|
+
export declare const getPgRootAdmin: (connOpts?: TestConnectionOptions) => DbAdmin;
|
|
7
|
+
export interface GetConnectionOpts {
|
|
8
|
+
pg?: Partial<PgConfig>;
|
|
9
|
+
db?: Partial<TestConnectionOptions>;
|
|
10
|
+
}
|
|
11
|
+
export interface GetConnectionResult {
|
|
12
|
+
pg: PgTestClient;
|
|
13
|
+
db: PgTestClient;
|
|
14
|
+
admin: DbAdmin;
|
|
15
|
+
teardown: () => Promise<void>;
|
|
16
|
+
manager: PgTestConnector;
|
|
17
|
+
}
|
|
18
|
+
export declare const getConnections: (cn?: GetConnectionOpts, seedAdapter?: SeedAdapter) => Promise<GetConnectionResult>;
|
package/connect.js
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getConnections = exports.getPgRootAdmin = void 0;
|
|
4
|
+
const admin_1 = require("./admin");
|
|
5
|
+
const types_1 = require("@launchql/types");
|
|
6
|
+
const migrate_1 = require("@launchql/migrate");
|
|
7
|
+
const manager_1 = require("./manager");
|
|
8
|
+
const crypto_1 = require("crypto");
|
|
9
|
+
const server_utils_1 = require("@launchql/server-utils");
|
|
10
|
+
let manager;
|
|
11
|
+
const getPgRootAdmin = (connOpts = {}) => {
|
|
12
|
+
const opts = (0, types_1.getPgEnvOptions)({
|
|
13
|
+
database: connOpts.rootDb
|
|
14
|
+
});
|
|
15
|
+
const admin = new admin_1.DbAdmin(opts);
|
|
16
|
+
return admin;
|
|
17
|
+
};
|
|
18
|
+
exports.getPgRootAdmin = getPgRootAdmin;
|
|
19
|
+
const getConnOopts = (cn = {}) => {
|
|
20
|
+
const connect = (0, types_1.getConnEnvOptions)(cn.db);
|
|
21
|
+
const config = (0, types_1.getPgEnvOptions)({
|
|
22
|
+
database: `${connect.prefix}${(0, crypto_1.randomUUID)()}`,
|
|
23
|
+
...cn.pg
|
|
24
|
+
});
|
|
25
|
+
return {
|
|
26
|
+
pg: config,
|
|
27
|
+
db: connect
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
const getConnections = async (cn = {}, seedAdapter) => {
|
|
31
|
+
cn = getConnOopts(cn);
|
|
32
|
+
const config = cn.pg;
|
|
33
|
+
const connOpts = cn.db;
|
|
34
|
+
const root = (0, exports.getPgRootAdmin)(connOpts);
|
|
35
|
+
await root.createUserRole(connOpts.connection.user, connOpts.connection.password, connOpts.rootDb);
|
|
36
|
+
const admin = new admin_1.DbAdmin(config);
|
|
37
|
+
const proj = new migrate_1.LaunchQLProject(connOpts.cwd);
|
|
38
|
+
if (proj.isInModule()) {
|
|
39
|
+
admin.create(config.database);
|
|
40
|
+
admin.installExtensions(connOpts.extensions);
|
|
41
|
+
const opts = (0, types_1.getEnvOptions)({
|
|
42
|
+
pg: config
|
|
43
|
+
});
|
|
44
|
+
if (connOpts.deployFast) {
|
|
45
|
+
await (0, migrate_1.deployFast)({
|
|
46
|
+
opts,
|
|
47
|
+
name: proj.getModuleName(),
|
|
48
|
+
database: config.database,
|
|
49
|
+
dir: proj.modulePath,
|
|
50
|
+
usePlan: true,
|
|
51
|
+
verbose: false
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
await (0, migrate_1.deploy)(opts, proj.getModuleName(), config.database, proj.modulePath);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
// Create the test database
|
|
60
|
+
if (process.env.TEST_DB) {
|
|
61
|
+
config.database = process.env.TEST_DB;
|
|
62
|
+
}
|
|
63
|
+
else if (connOpts.template) {
|
|
64
|
+
admin.createFromTemplate(connOpts.template, config.database);
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
admin.create(config.database);
|
|
68
|
+
admin.installExtensions(connOpts.extensions);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
await admin.grantConnect(connOpts.connection.user, config.database);
|
|
72
|
+
// Main admin client (optional unless needed elsewhere)
|
|
73
|
+
manager = manager_1.PgTestConnector.getInstance();
|
|
74
|
+
const pg = manager.getClient(config);
|
|
75
|
+
if (seedAdapter) {
|
|
76
|
+
await seedAdapter.seed({
|
|
77
|
+
admin,
|
|
78
|
+
config: config,
|
|
79
|
+
pg: manager.getClient(config)
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
// App user connection
|
|
83
|
+
const db = manager.getClient({
|
|
84
|
+
...config,
|
|
85
|
+
user: connOpts.connection.user,
|
|
86
|
+
password: connOpts.connection.password
|
|
87
|
+
});
|
|
88
|
+
db.setContext({ role: 'anonymous' });
|
|
89
|
+
const teardown = async () => {
|
|
90
|
+
await (0, server_utils_1.teardownPgPools)();
|
|
91
|
+
await manager.closeAll();
|
|
92
|
+
};
|
|
93
|
+
return { pg, db, teardown, manager, admin };
|
|
94
|
+
};
|
|
95
|
+
exports.getConnections = getConnections;
|