pgsql-test 2.19.1 → 2.20.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/README.md +5 -22
- package/admin.d.ts +2 -20
- package/admin.js +4 -124
- package/connect.d.ts +5 -1
- package/connect.js +6 -4
- package/esm/admin.js +4 -124
- package/esm/connect.js +5 -3
- package/esm/index.js +0 -1
- package/esm/manager.js +28 -17
- package/esm/test-client.js +3 -123
- package/index.d.ts +0 -1
- package/index.js +0 -1
- package/manager.d.ts +3 -1
- package/manager.js +28 -17
- package/package.json +4 -4
- package/test-client.d.ts +3 -39
- package/test-client.js +3 -123
- package/context-utils.d.ts +0 -8
- package/context-utils.js +0 -28
- package/esm/context-utils.js +0 -25
- package/esm/roles.js +0 -32
- package/esm/stream.js +0 -96
- package/roles.d.ts +0 -17
- package/roles.js +0 -38
- package/stream.d.ts +0 -33
- package/stream.js +0 -99
package/README.md
CHANGED
|
@@ -685,9 +685,14 @@ Common issues and solutions for pgpm, PostgreSQL, and testing.
|
|
|
685
685
|
|
|
686
686
|
## Related Constructive Tooling
|
|
687
687
|
|
|
688
|
+
### 📦 Package Management
|
|
689
|
+
|
|
690
|
+
* [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.
|
|
691
|
+
|
|
688
692
|
### 🧪 Testing
|
|
689
693
|
|
|
690
694
|
* [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.
|
|
695
|
+
* [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.
|
|
691
696
|
* [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.
|
|
692
697
|
* [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.
|
|
693
698
|
* [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.
|
|
@@ -701,28 +706,6 @@ Common issues and solutions for pgpm, PostgreSQL, and testing.
|
|
|
701
706
|
* [@pgsql/types](https://www.npmjs.com/package/@pgsql/types): **📝 Type definitions** for PostgreSQL AST nodes in TypeScript.
|
|
702
707
|
* [@pgsql/utils](https://www.npmjs.com/package/@pgsql/utils): **🛠️ AST utilities** for constructing and transforming PostgreSQL syntax trees.
|
|
703
708
|
|
|
704
|
-
### 🚀 API & Dev Tools
|
|
705
|
-
|
|
706
|
-
* [@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.
|
|
707
|
-
* [@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.
|
|
708
|
-
|
|
709
|
-
### 🔁 Streaming & Uploads
|
|
710
|
-
|
|
711
|
-
* [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.
|
|
712
|
-
* [etag-stream](https://github.com/constructive-io/constructive/tree/main/streaming/etag-stream): **🔄 ETag computation** via Node stream transformer during upload or transfer.
|
|
713
|
-
* [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.
|
|
714
|
-
* [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.
|
|
715
|
-
* [@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.
|
|
716
|
-
* [@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.
|
|
717
|
-
|
|
718
|
-
### 🧰 CLI & Codegen
|
|
719
|
-
|
|
720
|
-
* [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.
|
|
721
|
-
* [@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.
|
|
722
|
-
* [@constructive-io/graphql-codegen](https://github.com/constructive-io/constructive/tree/main/graphql/codegen): **✨ GraphQL code generation** (types, operations, SDK) from schema/endpoint introspection.
|
|
723
|
-
* [@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.
|
|
724
|
-
* [@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.
|
|
725
|
-
|
|
726
709
|
## Credits
|
|
727
710
|
|
|
728
711
|
**🛠 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
CHANGED
|
@@ -1,26 +1,8 @@
|
|
|
1
|
+
import { DbAdmin as BaseDbAdmin } from 'pgsql-client';
|
|
1
2
|
import { PgTestConnectionOptions } from '@pgpmjs/types';
|
|
2
3
|
import { PgConfig } from 'pg-env';
|
|
3
4
|
import { SeedAdapter } from './seed/types';
|
|
4
|
-
export declare class DbAdmin {
|
|
5
|
-
private config;
|
|
6
|
-
private verbose;
|
|
7
|
-
private roleConfig?;
|
|
5
|
+
export declare class DbAdmin extends BaseDbAdmin {
|
|
8
6
|
constructor(config: PgConfig, verbose?: boolean, roleConfig?: PgTestConnectionOptions);
|
|
9
|
-
private getEnv;
|
|
10
|
-
private run;
|
|
11
|
-
private safeDropDb;
|
|
12
|
-
drop(dbName?: string): void;
|
|
13
|
-
dropTemplate(dbName: string): void;
|
|
14
|
-
create(dbName?: string): void;
|
|
15
|
-
createFromTemplate(template: string, dbName?: string): void;
|
|
16
|
-
installExtensions(extensions: string[] | string, dbName?: string): void;
|
|
17
|
-
connectionString(dbName?: string): string;
|
|
18
|
-
createTemplateFromBase(base: string, template: string): void;
|
|
19
|
-
cleanupTemplate(template: string): void;
|
|
20
|
-
grantRole(role: string, user: string, dbName?: string): Promise<void>;
|
|
21
|
-
grantConnect(role: string, dbName?: string): Promise<void>;
|
|
22
|
-
createUserRole(user: string, password: string, dbName: string, useLocksForRoles?: boolean): Promise<void>;
|
|
23
|
-
loadSql(file: string, dbName: string): void;
|
|
24
|
-
streamSql(sql: string, dbName: string): Promise<void>;
|
|
25
7
|
createSeededTemplate(templateName: string, adapter: SeedAdapter): Promise<void>;
|
|
26
8
|
}
|
package/admin.js
CHANGED
|
@@ -1,131 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.DbAdmin = void 0;
|
|
4
|
-
const
|
|
5
|
-
|
|
6
|
-
|
|
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;
|
|
4
|
+
const pgsql_client_1 = require("pgsql-client");
|
|
5
|
+
// Extend DbAdmin from pgsql-client with test-specific methods
|
|
6
|
+
class DbAdmin extends pgsql_client_1.DbAdmin {
|
|
16
7
|
constructor(config, verbose = false, roleConfig) {
|
|
17
|
-
|
|
18
|
-
this.verbose = verbose;
|
|
19
|
-
this.roleConfig = roleConfig;
|
|
20
|
-
this.config = (0, pg_env_1.getPgEnvOptions)(config);
|
|
21
|
-
}
|
|
22
|
-
getEnv() {
|
|
23
|
-
return {
|
|
24
|
-
PGHOST: this.config.host,
|
|
25
|
-
PGPORT: String(this.config.port),
|
|
26
|
-
PGUSER: this.config.user,
|
|
27
|
-
PGPASSWORD: this.config.password
|
|
28
|
-
};
|
|
29
|
-
}
|
|
30
|
-
run(command) {
|
|
31
|
-
try {
|
|
32
|
-
(0, child_process_1.execSync)(command, {
|
|
33
|
-
stdio: this.verbose ? 'inherit' : 'pipe',
|
|
34
|
-
env: {
|
|
35
|
-
...process.env,
|
|
36
|
-
...this.getEnv()
|
|
37
|
-
}
|
|
38
|
-
});
|
|
39
|
-
if (this.verbose)
|
|
40
|
-
log.success(`Executed: ${command}`);
|
|
41
|
-
}
|
|
42
|
-
catch (err) {
|
|
43
|
-
log.error(`Command failed: ${command}`);
|
|
44
|
-
if (this.verbose)
|
|
45
|
-
log.error(err.message);
|
|
46
|
-
throw err;
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
safeDropDb(name) {
|
|
50
|
-
try {
|
|
51
|
-
this.run(`dropdb "${name}"`);
|
|
52
|
-
}
|
|
53
|
-
catch (err) {
|
|
54
|
-
if (!err.message.includes('does not exist')) {
|
|
55
|
-
log.warn(`Could not drop database ${name}: ${err.message}`);
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
drop(dbName) {
|
|
60
|
-
this.safeDropDb(dbName ?? this.config.database);
|
|
61
|
-
}
|
|
62
|
-
dropTemplate(dbName) {
|
|
63
|
-
this.run(`psql -c "UPDATE pg_database SET datistemplate='false' WHERE datname='${dbName}';"`);
|
|
64
|
-
this.drop(dbName);
|
|
65
|
-
}
|
|
66
|
-
create(dbName) {
|
|
67
|
-
const db = dbName ?? this.config.database;
|
|
68
|
-
this.run(`createdb -U ${this.config.user} -h ${this.config.host} -p ${this.config.port} "${db}"`);
|
|
69
|
-
}
|
|
70
|
-
createFromTemplate(template, dbName) {
|
|
71
|
-
const db = dbName ?? this.config.database;
|
|
72
|
-
this.run(`createdb -U ${this.config.user} -h ${this.config.host} -p ${this.config.port} -e "${db}" -T "${template}"`);
|
|
73
|
-
}
|
|
74
|
-
installExtensions(extensions, dbName) {
|
|
75
|
-
const db = dbName ?? this.config.database;
|
|
76
|
-
const extList = typeof extensions === 'string' ? extensions.split(',') : extensions;
|
|
77
|
-
for (const extension of extList) {
|
|
78
|
-
this.run(`psql --dbname "${db}" -c 'CREATE EXTENSION IF NOT EXISTS "${extension}" CASCADE;'`);
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
connectionString(dbName) {
|
|
82
|
-
const { user, password, host, port } = this.config;
|
|
83
|
-
const db = dbName ?? this.config.database;
|
|
84
|
-
return `postgres://${user}:${password}@${host}:${port}/${db}`;
|
|
85
|
-
}
|
|
86
|
-
createTemplateFromBase(base, template) {
|
|
87
|
-
this.run(`createdb -T "${base}" "${template}"`);
|
|
88
|
-
this.run(`psql -c "UPDATE pg_database SET datistemplate = true WHERE datname = '${template}';"`);
|
|
89
|
-
}
|
|
90
|
-
cleanupTemplate(template) {
|
|
91
|
-
try {
|
|
92
|
-
this.run(`psql -c "UPDATE pg_database SET datistemplate = false WHERE datname = '${template}'"`);
|
|
93
|
-
}
|
|
94
|
-
catch {
|
|
95
|
-
log.warn(`Skipping failed UPDATE of datistemplate for ${template}`);
|
|
96
|
-
}
|
|
97
|
-
this.safeDropDb(template);
|
|
98
|
-
}
|
|
99
|
-
async grantRole(role, user, dbName) {
|
|
100
|
-
const db = dbName ?? this.config.database;
|
|
101
|
-
const sql = (0, core_1.generateGrantRoleSQL)(role, user);
|
|
102
|
-
await this.streamSql(sql, db);
|
|
103
|
-
}
|
|
104
|
-
async grantConnect(role, dbName) {
|
|
105
|
-
const db = dbName ?? this.config.database;
|
|
106
|
-
const sql = `GRANT CONNECT ON DATABASE "${db}" TO ${role};`;
|
|
107
|
-
await this.streamSql(sql, db);
|
|
108
|
-
}
|
|
109
|
-
// ONLY granting admin role for testing purposes, normally the db connection for apps won't have admin role
|
|
110
|
-
// DO NOT USE THIS FOR PRODUCTION
|
|
111
|
-
async createUserRole(user, password, dbName, useLocksForRoles = false) {
|
|
112
|
-
const anonRole = (0, roles_1.getRoleName)('anonymous', this.roleConfig);
|
|
113
|
-
const authRole = (0, roles_1.getRoleName)('authenticated', this.roleConfig);
|
|
114
|
-
const adminRole = (0, roles_1.getRoleName)('administrator', this.roleConfig);
|
|
115
|
-
const sql = (0, core_1.generateCreateUserWithGrantsSQL)(user, password, [anonRole, authRole, adminRole], useLocksForRoles);
|
|
116
|
-
await this.streamSql(sql, dbName);
|
|
117
|
-
}
|
|
118
|
-
loadSql(file, dbName) {
|
|
119
|
-
if (!(0, fs_1.existsSync)(file)) {
|
|
120
|
-
throw new Error(`Missing SQL file: ${file}`);
|
|
121
|
-
}
|
|
122
|
-
this.run(`psql -f ${file} ${dbName}`);
|
|
123
|
-
}
|
|
124
|
-
async streamSql(sql, dbName) {
|
|
125
|
-
await (0, stream_1.streamSql)({
|
|
126
|
-
...this.config,
|
|
127
|
-
database: dbName
|
|
128
|
-
}, sql);
|
|
8
|
+
super(config, verbose, roleConfig);
|
|
129
9
|
}
|
|
130
10
|
async createSeededTemplate(templateName, adapter) {
|
|
131
11
|
const seedDb = this.config.database;
|
package/connect.d.ts
CHANGED
|
@@ -9,11 +9,15 @@ export interface GetConnectionOpts {
|
|
|
9
9
|
pg?: Partial<PgConfig>;
|
|
10
10
|
db?: Partial<PgTestConnectionOptions>;
|
|
11
11
|
}
|
|
12
|
+
export interface TeardownOptions {
|
|
13
|
+
/** If true, keeps the database after closing connections (default: false) */
|
|
14
|
+
keepDb?: boolean;
|
|
15
|
+
}
|
|
12
16
|
export interface GetConnectionResult {
|
|
13
17
|
pg: PgTestClient;
|
|
14
18
|
db: PgTestClient;
|
|
15
19
|
admin: DbAdmin;
|
|
16
|
-
teardown: () => Promise<void>;
|
|
20
|
+
teardown: (opts?: TeardownOptions) => Promise<void>;
|
|
17
21
|
manager: PgTestConnector;
|
|
18
22
|
}
|
|
19
23
|
export declare const getConnections: (cn?: GetConnectionOpts, seedAdapters?: SeedAdapter[]) => Promise<GetConnectionResult>;
|
package/connect.js
CHANGED
|
@@ -5,9 +5,9 @@ const env_1 = require("@pgpmjs/env");
|
|
|
5
5
|
const crypto_1 = require("crypto");
|
|
6
6
|
const pg_cache_1 = require("pg-cache");
|
|
7
7
|
const pg_env_1 = require("pg-env");
|
|
8
|
+
const pgsql_client_1 = require("pgsql-client");
|
|
8
9
|
const admin_1 = require("./admin");
|
|
9
10
|
const manager_1 = require("./manager");
|
|
10
|
-
const roles_1 = require("./roles");
|
|
11
11
|
const seed_1 = require("./seed");
|
|
12
12
|
let manager;
|
|
13
13
|
const getPgRootAdmin = (config, connOpts = {}) => {
|
|
@@ -54,13 +54,15 @@ const getConnections = async (cn = {}, seedAdapters = [seed_1.seed.pgpm()]) => {
|
|
|
54
54
|
manager = manager_1.PgTestConnector.getInstance(config);
|
|
55
55
|
const pg = manager.getClient(config);
|
|
56
56
|
let teardownPromise = null;
|
|
57
|
-
|
|
57
|
+
let teardownOpts = {};
|
|
58
|
+
const teardown = async (opts = {}) => {
|
|
59
|
+
teardownOpts = opts;
|
|
58
60
|
if (teardownPromise)
|
|
59
61
|
return teardownPromise;
|
|
60
62
|
teardownPromise = (async () => {
|
|
61
63
|
manager.beginTeardown();
|
|
62
64
|
await (0, pg_cache_1.teardownPgPools)();
|
|
63
|
-
await manager.closeAll();
|
|
65
|
+
await manager.closeAll({ keepDb: teardownOpts.keepDb });
|
|
64
66
|
})();
|
|
65
67
|
return teardownPromise;
|
|
66
68
|
};
|
|
@@ -89,7 +91,7 @@ const getConnections = async (cn = {}, seedAdapters = [seed_1.seed.pgpm()]) => {
|
|
|
89
91
|
auth: connOpts.auth,
|
|
90
92
|
roles: connOpts.roles
|
|
91
93
|
});
|
|
92
|
-
db.setContext({ role: (0,
|
|
94
|
+
db.setContext({ role: (0, pgsql_client_1.getDefaultRole)(connOpts) });
|
|
93
95
|
return { pg, db, teardown, manager, admin };
|
|
94
96
|
};
|
|
95
97
|
exports.getConnections = getConnections;
|
package/esm/admin.js
CHANGED
|
@@ -1,128 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
import { existsSync } from 'fs';
|
|
5
|
-
import { getPgEnvOptions } from 'pg-env';
|
|
6
|
-
import { getRoleName } from './roles';
|
|
7
|
-
import { streamSql as stream } from './stream';
|
|
8
|
-
const log = new Logger('db-admin');
|
|
9
|
-
export class DbAdmin {
|
|
10
|
-
config;
|
|
11
|
-
verbose;
|
|
12
|
-
roleConfig;
|
|
1
|
+
import { DbAdmin as BaseDbAdmin } from 'pgsql-client';
|
|
2
|
+
// Extend DbAdmin from pgsql-client with test-specific methods
|
|
3
|
+
export class DbAdmin extends BaseDbAdmin {
|
|
13
4
|
constructor(config, verbose = false, roleConfig) {
|
|
14
|
-
|
|
15
|
-
this.verbose = verbose;
|
|
16
|
-
this.roleConfig = roleConfig;
|
|
17
|
-
this.config = getPgEnvOptions(config);
|
|
18
|
-
}
|
|
19
|
-
getEnv() {
|
|
20
|
-
return {
|
|
21
|
-
PGHOST: this.config.host,
|
|
22
|
-
PGPORT: String(this.config.port),
|
|
23
|
-
PGUSER: this.config.user,
|
|
24
|
-
PGPASSWORD: this.config.password
|
|
25
|
-
};
|
|
26
|
-
}
|
|
27
|
-
run(command) {
|
|
28
|
-
try {
|
|
29
|
-
execSync(command, {
|
|
30
|
-
stdio: this.verbose ? 'inherit' : 'pipe',
|
|
31
|
-
env: {
|
|
32
|
-
...process.env,
|
|
33
|
-
...this.getEnv()
|
|
34
|
-
}
|
|
35
|
-
});
|
|
36
|
-
if (this.verbose)
|
|
37
|
-
log.success(`Executed: ${command}`);
|
|
38
|
-
}
|
|
39
|
-
catch (err) {
|
|
40
|
-
log.error(`Command failed: ${command}`);
|
|
41
|
-
if (this.verbose)
|
|
42
|
-
log.error(err.message);
|
|
43
|
-
throw err;
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
safeDropDb(name) {
|
|
47
|
-
try {
|
|
48
|
-
this.run(`dropdb "${name}"`);
|
|
49
|
-
}
|
|
50
|
-
catch (err) {
|
|
51
|
-
if (!err.message.includes('does not exist')) {
|
|
52
|
-
log.warn(`Could not drop database ${name}: ${err.message}`);
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
drop(dbName) {
|
|
57
|
-
this.safeDropDb(dbName ?? this.config.database);
|
|
58
|
-
}
|
|
59
|
-
dropTemplate(dbName) {
|
|
60
|
-
this.run(`psql -c "UPDATE pg_database SET datistemplate='false' WHERE datname='${dbName}';"`);
|
|
61
|
-
this.drop(dbName);
|
|
62
|
-
}
|
|
63
|
-
create(dbName) {
|
|
64
|
-
const db = dbName ?? this.config.database;
|
|
65
|
-
this.run(`createdb -U ${this.config.user} -h ${this.config.host} -p ${this.config.port} "${db}"`);
|
|
66
|
-
}
|
|
67
|
-
createFromTemplate(template, dbName) {
|
|
68
|
-
const db = dbName ?? this.config.database;
|
|
69
|
-
this.run(`createdb -U ${this.config.user} -h ${this.config.host} -p ${this.config.port} -e "${db}" -T "${template}"`);
|
|
70
|
-
}
|
|
71
|
-
installExtensions(extensions, dbName) {
|
|
72
|
-
const db = dbName ?? this.config.database;
|
|
73
|
-
const extList = typeof extensions === 'string' ? extensions.split(',') : extensions;
|
|
74
|
-
for (const extension of extList) {
|
|
75
|
-
this.run(`psql --dbname "${db}" -c 'CREATE EXTENSION IF NOT EXISTS "${extension}" CASCADE;'`);
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
connectionString(dbName) {
|
|
79
|
-
const { user, password, host, port } = this.config;
|
|
80
|
-
const db = dbName ?? this.config.database;
|
|
81
|
-
return `postgres://${user}:${password}@${host}:${port}/${db}`;
|
|
82
|
-
}
|
|
83
|
-
createTemplateFromBase(base, template) {
|
|
84
|
-
this.run(`createdb -T "${base}" "${template}"`);
|
|
85
|
-
this.run(`psql -c "UPDATE pg_database SET datistemplate = true WHERE datname = '${template}';"`);
|
|
86
|
-
}
|
|
87
|
-
cleanupTemplate(template) {
|
|
88
|
-
try {
|
|
89
|
-
this.run(`psql -c "UPDATE pg_database SET datistemplate = false WHERE datname = '${template}'"`);
|
|
90
|
-
}
|
|
91
|
-
catch {
|
|
92
|
-
log.warn(`Skipping failed UPDATE of datistemplate for ${template}`);
|
|
93
|
-
}
|
|
94
|
-
this.safeDropDb(template);
|
|
95
|
-
}
|
|
96
|
-
async grantRole(role, user, dbName) {
|
|
97
|
-
const db = dbName ?? this.config.database;
|
|
98
|
-
const sql = generateGrantRoleSQL(role, user);
|
|
99
|
-
await this.streamSql(sql, db);
|
|
100
|
-
}
|
|
101
|
-
async grantConnect(role, dbName) {
|
|
102
|
-
const db = dbName ?? this.config.database;
|
|
103
|
-
const sql = `GRANT CONNECT ON DATABASE "${db}" TO ${role};`;
|
|
104
|
-
await this.streamSql(sql, db);
|
|
105
|
-
}
|
|
106
|
-
// ONLY granting admin role for testing purposes, normally the db connection for apps won't have admin role
|
|
107
|
-
// DO NOT USE THIS FOR PRODUCTION
|
|
108
|
-
async createUserRole(user, password, dbName, useLocksForRoles = false) {
|
|
109
|
-
const anonRole = getRoleName('anonymous', this.roleConfig);
|
|
110
|
-
const authRole = getRoleName('authenticated', this.roleConfig);
|
|
111
|
-
const adminRole = getRoleName('administrator', this.roleConfig);
|
|
112
|
-
const sql = generateCreateUserWithGrantsSQL(user, password, [anonRole, authRole, adminRole], useLocksForRoles);
|
|
113
|
-
await this.streamSql(sql, dbName);
|
|
114
|
-
}
|
|
115
|
-
loadSql(file, dbName) {
|
|
116
|
-
if (!existsSync(file)) {
|
|
117
|
-
throw new Error(`Missing SQL file: ${file}`);
|
|
118
|
-
}
|
|
119
|
-
this.run(`psql -f ${file} ${dbName}`);
|
|
120
|
-
}
|
|
121
|
-
async streamSql(sql, dbName) {
|
|
122
|
-
await stream({
|
|
123
|
-
...this.config,
|
|
124
|
-
database: dbName
|
|
125
|
-
}, sql);
|
|
5
|
+
super(config, verbose, roleConfig);
|
|
126
6
|
}
|
|
127
7
|
async createSeededTemplate(templateName, adapter) {
|
|
128
8
|
const seedDb = this.config.database;
|
package/esm/connect.js
CHANGED
|
@@ -2,9 +2,9 @@ import { getConnEnvOptions } from '@pgpmjs/env';
|
|
|
2
2
|
import { randomUUID } from 'crypto';
|
|
3
3
|
import { teardownPgPools } from 'pg-cache';
|
|
4
4
|
import { getPgEnvOptions, } from 'pg-env';
|
|
5
|
+
import { getDefaultRole } from 'pgsql-client';
|
|
5
6
|
import { DbAdmin } from './admin';
|
|
6
7
|
import { PgTestConnector } from './manager';
|
|
7
|
-
import { getDefaultRole } from './roles';
|
|
8
8
|
import { seed } from './seed';
|
|
9
9
|
let manager;
|
|
10
10
|
export const getPgRootAdmin = (config, connOpts = {}) => {
|
|
@@ -50,13 +50,15 @@ export const getConnections = async (cn = {}, seedAdapters = [seed.pgpm()]) => {
|
|
|
50
50
|
manager = PgTestConnector.getInstance(config);
|
|
51
51
|
const pg = manager.getClient(config);
|
|
52
52
|
let teardownPromise = null;
|
|
53
|
-
|
|
53
|
+
let teardownOpts = {};
|
|
54
|
+
const teardown = async (opts = {}) => {
|
|
55
|
+
teardownOpts = opts;
|
|
54
56
|
if (teardownPromise)
|
|
55
57
|
return teardownPromise;
|
|
56
58
|
teardownPromise = (async () => {
|
|
57
59
|
manager.beginTeardown();
|
|
58
60
|
await teardownPgPools();
|
|
59
|
-
await manager.closeAll();
|
|
61
|
+
await manager.closeAll({ keepDb: teardownOpts.keepDb });
|
|
60
62
|
})();
|
|
61
63
|
return teardownPromise;
|
|
62
64
|
};
|
package/esm/index.js
CHANGED
package/esm/manager.js
CHANGED
|
@@ -5,13 +5,13 @@ import { DbAdmin } from './admin';
|
|
|
5
5
|
import { PgTestClient } from './test-client';
|
|
6
6
|
const log = new Logger('test-connector');
|
|
7
7
|
const SYS_EVENTS = ['SIGTERM'];
|
|
8
|
-
const end = (pool) => {
|
|
8
|
+
const end = async (pool) => {
|
|
9
9
|
try {
|
|
10
10
|
if (pool.ended || pool.ending) {
|
|
11
11
|
log.warn('⚠️ pg pool already ended or ending');
|
|
12
12
|
return;
|
|
13
13
|
}
|
|
14
|
-
pool.end();
|
|
14
|
+
await pool.end();
|
|
15
15
|
}
|
|
16
16
|
catch (err) {
|
|
17
17
|
log.error('❌ pg pool termination error:', err);
|
|
@@ -84,7 +84,8 @@ export class PgTestConnector {
|
|
|
84
84
|
log.info(`🔌 New PgTestClient connected to ${config.database}`);
|
|
85
85
|
return client;
|
|
86
86
|
}
|
|
87
|
-
async closeAll() {
|
|
87
|
+
async closeAll(opts = {}) {
|
|
88
|
+
const { keepDb = false } = opts;
|
|
88
89
|
this.beginTeardown();
|
|
89
90
|
await this.awaitPendingConnects();
|
|
90
91
|
log.info('🧹 Closing all PgTestClients...');
|
|
@@ -99,25 +100,35 @@ export class PgTestConnector {
|
|
|
99
100
|
}));
|
|
100
101
|
this.clients.clear();
|
|
101
102
|
log.info('🧯 Disposing pg pools...');
|
|
103
|
+
const poolTasks = [];
|
|
102
104
|
for (const [key, pool] of this.pgPools.entries()) {
|
|
103
105
|
log.debug(`🧯 Disposing pg pool [${key}]`);
|
|
104
|
-
end(pool);
|
|
106
|
+
poolTasks.push(end(pool));
|
|
105
107
|
}
|
|
106
108
|
this.pgPools.clear();
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
109
|
+
await Promise.allSettled(poolTasks);
|
|
110
|
+
if (keepDb) {
|
|
111
|
+
log.info('📦 Keeping databases (keepDb=true)...');
|
|
112
|
+
const dbNames = Array.from(this.seenDbConfigs.values()).map(c => c.database);
|
|
113
|
+
log.info(`📦 Preserved databases: ${dbNames.join(', ')}`);
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
log.info('🗑️ Dropping seen databases...');
|
|
117
|
+
await Promise.all(Array.from(this.seenDbConfigs.values()).map(async (config) => {
|
|
118
|
+
try {
|
|
119
|
+
const rootPg = getPgEnvOptions(this.config);
|
|
120
|
+
const admin = new DbAdmin({ ...config, user: rootPg.user, password: rootPg.password }, this.verbose);
|
|
121
|
+
admin.drop();
|
|
122
|
+
log.warn(`🧨 Dropped database: ${config.database}`);
|
|
123
|
+
}
|
|
124
|
+
catch (err) {
|
|
125
|
+
log.error(`❌ Failed to drop database ${config.database}:`, err);
|
|
126
|
+
}
|
|
127
|
+
}));
|
|
128
|
+
}
|
|
119
129
|
this.seenDbConfigs.clear();
|
|
120
|
-
|
|
130
|
+
const action = keepDb ? 'preserved' : 'dropped';
|
|
131
|
+
log.success(`✅ All PgTestClients closed, pools disposed, databases ${action}.`);
|
|
121
132
|
this.pendingConnects.clear();
|
|
122
133
|
this.shuttingDown = false;
|
|
123
134
|
}
|
package/esm/test-client.js
CHANGED
|
@@ -1,60 +1,11 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { getRoleName } from './roles';
|
|
3
|
-
import { generateContextStatements } from './context-utils';
|
|
1
|
+
import { PgClient } from 'pgsql-client';
|
|
4
2
|
import { insertJsonMap } from 'pgsql-seed';
|
|
5
3
|
import { loadCsvMap } from 'pgsql-seed';
|
|
6
4
|
import { loadSqlFiles } from 'pgsql-seed';
|
|
7
5
|
import { deployPgpm } from 'pgsql-seed';
|
|
8
|
-
export class PgTestClient {
|
|
9
|
-
config;
|
|
10
|
-
client;
|
|
11
|
-
opts;
|
|
12
|
-
ctxStmts = '';
|
|
13
|
-
contextSettings = {};
|
|
14
|
-
_ended = false;
|
|
15
|
-
connectPromise = null;
|
|
6
|
+
export class PgTestClient extends PgClient {
|
|
16
7
|
constructor(config, opts = {}) {
|
|
17
|
-
|
|
18
|
-
this.config = config;
|
|
19
|
-
this.client = new Client({
|
|
20
|
-
host: this.config.host,
|
|
21
|
-
port: this.config.port,
|
|
22
|
-
database: this.config.database,
|
|
23
|
-
user: this.config.user,
|
|
24
|
-
password: this.config.password
|
|
25
|
-
});
|
|
26
|
-
if (!opts.deferConnect) {
|
|
27
|
-
this.connectPromise = this.client.connect();
|
|
28
|
-
if (opts.trackConnect)
|
|
29
|
-
opts.trackConnect(this.connectPromise);
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
async ensureConnected() {
|
|
33
|
-
if (this.connectPromise) {
|
|
34
|
-
try {
|
|
35
|
-
await this.connectPromise;
|
|
36
|
-
}
|
|
37
|
-
catch { }
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
async close() {
|
|
41
|
-
if (!this._ended) {
|
|
42
|
-
this._ended = true;
|
|
43
|
-
await this.ensureConnected();
|
|
44
|
-
this.client.end();
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
async begin() {
|
|
48
|
-
await this.client.query('BEGIN;');
|
|
49
|
-
}
|
|
50
|
-
async savepoint(name = 'lqlsavepoint') {
|
|
51
|
-
await this.client.query(`SAVEPOINT "${name}";`);
|
|
52
|
-
}
|
|
53
|
-
async rollback(name = 'lqlsavepoint') {
|
|
54
|
-
await this.client.query(`ROLLBACK TO SAVEPOINT "${name}";`);
|
|
55
|
-
}
|
|
56
|
-
async commit() {
|
|
57
|
-
await this.client.query('COMMIT;');
|
|
8
|
+
super(config, opts);
|
|
58
9
|
}
|
|
59
10
|
async beforeEach() {
|
|
60
11
|
await this.begin();
|
|
@@ -64,23 +15,6 @@ export class PgTestClient {
|
|
|
64
15
|
await this.rollback();
|
|
65
16
|
await this.commit();
|
|
66
17
|
}
|
|
67
|
-
setContext(ctx) {
|
|
68
|
-
Object.assign(this.contextSettings, ctx);
|
|
69
|
-
this.ctxStmts = generateContextStatements(this.contextSettings);
|
|
70
|
-
}
|
|
71
|
-
/**
|
|
72
|
-
* Set authentication context for the current session.
|
|
73
|
-
* Configures role and user ID using cascading defaults from options → opts.auth → RoleMapping.
|
|
74
|
-
*/
|
|
75
|
-
auth(options = {}) {
|
|
76
|
-
const role = options.role ?? this.opts.auth?.role ?? getRoleName('authenticated', this.opts);
|
|
77
|
-
const userIdKey = options.userIdKey ?? this.opts.auth?.userIdKey ?? 'jwt.claims.user_id';
|
|
78
|
-
const userId = options.userId ?? this.opts.auth?.userId ?? null;
|
|
79
|
-
this.setContext({
|
|
80
|
-
role,
|
|
81
|
-
[userIdKey]: userId !== null ? String(userId) : null
|
|
82
|
-
});
|
|
83
|
-
}
|
|
84
18
|
/**
|
|
85
19
|
* Commit current transaction to make data visible to other connections, then start fresh transaction.
|
|
86
20
|
* Maintains test isolation by creating a savepoint and reapplying session context.
|
|
@@ -91,60 +25,6 @@ export class PgTestClient {
|
|
|
91
25
|
await this.savepoint(); // keep rollback harness
|
|
92
26
|
await this.ctxQuery(); // reapply all setContext()
|
|
93
27
|
}
|
|
94
|
-
/**
|
|
95
|
-
* Clear all session context variables and reset to default anonymous role.
|
|
96
|
-
*/
|
|
97
|
-
clearContext() {
|
|
98
|
-
const defaultRole = getRoleName('anonymous', this.opts);
|
|
99
|
-
const nulledSettings = {};
|
|
100
|
-
Object.keys(this.contextSettings).forEach(key => {
|
|
101
|
-
nulledSettings[key] = null;
|
|
102
|
-
});
|
|
103
|
-
nulledSettings.role = defaultRole;
|
|
104
|
-
this.ctxStmts = generateContextStatements(nulledSettings);
|
|
105
|
-
this.contextSettings = { role: defaultRole };
|
|
106
|
-
}
|
|
107
|
-
async any(query, values) {
|
|
108
|
-
const result = await this.query(query, values);
|
|
109
|
-
return result.rows;
|
|
110
|
-
}
|
|
111
|
-
async one(query, values) {
|
|
112
|
-
const rows = await this.any(query, values);
|
|
113
|
-
if (rows.length !== 1) {
|
|
114
|
-
throw new Error('Expected exactly one result');
|
|
115
|
-
}
|
|
116
|
-
return rows[0];
|
|
117
|
-
}
|
|
118
|
-
async oneOrNone(query, values) {
|
|
119
|
-
const rows = await this.any(query, values);
|
|
120
|
-
return rows[0] || null;
|
|
121
|
-
}
|
|
122
|
-
async many(query, values) {
|
|
123
|
-
const rows = await this.any(query, values);
|
|
124
|
-
if (rows.length === 0)
|
|
125
|
-
throw new Error('Expected many rows, got none');
|
|
126
|
-
return rows;
|
|
127
|
-
}
|
|
128
|
-
async manyOrNone(query, values) {
|
|
129
|
-
return this.any(query, values);
|
|
130
|
-
}
|
|
131
|
-
async none(query, values) {
|
|
132
|
-
await this.query(query, values);
|
|
133
|
-
}
|
|
134
|
-
async result(query, values) {
|
|
135
|
-
return this.query(query, values);
|
|
136
|
-
}
|
|
137
|
-
async ctxQuery() {
|
|
138
|
-
if (this.ctxStmts) {
|
|
139
|
-
await this.client.query(this.ctxStmts);
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
// NOTE: all queries should call ctxQuery() before executing the query
|
|
143
|
-
async query(query, values) {
|
|
144
|
-
await this.ctxQuery();
|
|
145
|
-
const result = await this.client.query(query, values);
|
|
146
|
-
return result;
|
|
147
|
-
}
|
|
148
28
|
async loadJson(data) {
|
|
149
29
|
await this.ctxQuery();
|
|
150
30
|
await insertJsonMap(this.client, data);
|