pgsql-test 2.10.0 → 2.11.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/admin.js CHANGED
@@ -105,17 +105,50 @@ class DbAdmin {
105
105
  const sql = `GRANT CONNECT ON DATABASE "${db}" TO ${role};`;
106
106
  await this.streamSql(sql, db);
107
107
  }
108
+ // TODO: make adminRole a configurable option
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
108
111
  async createUserRole(user, password, dbName) {
109
112
  const anonRole = (0, roles_1.getRoleName)('anonymous', this.roleConfig);
110
113
  const authRole = (0, roles_1.getRoleName)('authenticated', this.roleConfig);
114
+ const adminRole = (0, roles_1.getRoleName)('administrator', this.roleConfig);
111
115
  const sql = `
112
116
  DO $$
113
117
  BEGIN
118
+ -- Create role if it doesn't exist
114
119
  IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = '${user}') THEN
115
120
  CREATE ROLE ${user} LOGIN PASSWORD '${password}';
121
+ END IF;
122
+
123
+ -- Grant anonymous role if not already granted
124
+ IF NOT EXISTS (
125
+ SELECT 1 FROM pg_auth_members am
126
+ JOIN pg_roles r1 ON am.roleid = r1.oid
127
+ JOIN pg_roles r2 ON am.member = r2.oid
128
+ WHERE r1.rolname = '${anonRole}' AND r2.rolname = '${user}'
129
+ ) THEN
116
130
  GRANT ${anonRole} TO ${user};
131
+ END IF;
132
+
133
+ -- Grant authenticated role if not already granted
134
+ IF NOT EXISTS (
135
+ SELECT 1 FROM pg_auth_members am
136
+ JOIN pg_roles r1 ON am.roleid = r1.oid
137
+ JOIN pg_roles r2 ON am.member = r2.oid
138
+ WHERE r1.rolname = '${authRole}' AND r2.rolname = '${user}'
139
+ ) THEN
117
140
  GRANT ${authRole} TO ${user};
118
141
  END IF;
142
+
143
+ -- Grant administrator role if not already granted
144
+ IF NOT EXISTS (
145
+ SELECT 1 FROM pg_auth_members am
146
+ JOIN pg_roles r1 ON am.roleid = r1.oid
147
+ JOIN pg_roles r2 ON am.member = r2.oid
148
+ WHERE r1.rolname = '${adminRole}' AND r2.rolname = '${user}'
149
+ ) THEN
150
+ GRANT ${adminRole} TO ${user};
151
+ END IF;
119
152
  END $$;
120
153
  `.trim();
121
154
  await this.streamSql(sql, dbName);
package/connect.js CHANGED
@@ -68,10 +68,14 @@ const getConnections = async (cn = {}, seedAdapters = [seed_1.seed.launchql()])
68
68
  throw error;
69
69
  }
70
70
  }
71
- const db = manager.getClient({
71
+ const dbConfig = {
72
72
  ...config,
73
73
  user: connOpts.connection.user,
74
74
  password: connOpts.connection.password
75
+ };
76
+ const db = manager.getClient(dbConfig, {
77
+ auth: connOpts.auth,
78
+ roles: connOpts.roles
75
79
  });
76
80
  db.setContext({ role: (0, roles_1.getDefaultRole)(connOpts) });
77
81
  return { pg, db, teardown, manager, admin };
package/esm/admin.js CHANGED
@@ -102,17 +102,50 @@ export class DbAdmin {
102
102
  const sql = `GRANT CONNECT ON DATABASE "${db}" TO ${role};`;
103
103
  await this.streamSql(sql, db);
104
104
  }
105
+ // TODO: make adminRole a configurable option
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
105
108
  async createUserRole(user, password, dbName) {
106
109
  const anonRole = getRoleName('anonymous', this.roleConfig);
107
110
  const authRole = getRoleName('authenticated', this.roleConfig);
111
+ const adminRole = getRoleName('administrator', this.roleConfig);
108
112
  const sql = `
109
113
  DO $$
110
114
  BEGIN
115
+ -- Create role if it doesn't exist
111
116
  IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = '${user}') THEN
112
117
  CREATE ROLE ${user} LOGIN PASSWORD '${password}';
118
+ END IF;
119
+
120
+ -- Grant anonymous role if not already granted
121
+ IF NOT EXISTS (
122
+ SELECT 1 FROM pg_auth_members am
123
+ JOIN pg_roles r1 ON am.roleid = r1.oid
124
+ JOIN pg_roles r2 ON am.member = r2.oid
125
+ WHERE r1.rolname = '${anonRole}' AND r2.rolname = '${user}'
126
+ ) THEN
113
127
  GRANT ${anonRole} TO ${user};
128
+ END IF;
129
+
130
+ -- Grant authenticated role if not already granted
131
+ IF NOT EXISTS (
132
+ SELECT 1 FROM pg_auth_members am
133
+ JOIN pg_roles r1 ON am.roleid = r1.oid
134
+ JOIN pg_roles r2 ON am.member = r2.oid
135
+ WHERE r1.rolname = '${authRole}' AND r2.rolname = '${user}'
136
+ ) THEN
114
137
  GRANT ${authRole} TO ${user};
115
138
  END IF;
139
+
140
+ -- Grant administrator role if not already granted
141
+ IF NOT EXISTS (
142
+ SELECT 1 FROM pg_auth_members am
143
+ JOIN pg_roles r1 ON am.roleid = r1.oid
144
+ JOIN pg_roles r2 ON am.member = r2.oid
145
+ WHERE r1.rolname = '${adminRole}' AND r2.rolname = '${user}'
146
+ ) THEN
147
+ GRANT ${adminRole} TO ${user};
148
+ END IF;
116
149
  END $$;
117
150
  `.trim();
118
151
  await this.streamSql(sql, dbName);
package/esm/connect.js CHANGED
@@ -64,10 +64,14 @@ export const getConnections = async (cn = {}, seedAdapters = [seed.launchql()])
64
64
  throw error;
65
65
  }
66
66
  }
67
- const db = manager.getClient({
67
+ const dbConfig = {
68
68
  ...config,
69
69
  user: connOpts.connection.user,
70
70
  password: connOpts.connection.password
71
+ };
72
+ const db = manager.getClient(dbConfig, {
73
+ auth: connOpts.auth,
74
+ roles: connOpts.roles
71
75
  });
72
76
  db.setContext({ role: getDefaultRole(connOpts) });
73
77
  return { pg, db, teardown, manager, admin };
package/esm/manager.js CHANGED
@@ -68,11 +68,14 @@ export class PgTestConnector {
68
68
  }
69
69
  return this.pgPools.get(key);
70
70
  }
71
- getClient(config) {
71
+ getClient(config, opts = {}) {
72
72
  if (this.shuttingDown) {
73
73
  throw new Error('PgTestConnector is shutting down; no new clients allowed');
74
74
  }
75
- const client = new PgTestClient(config, { trackConnect: (p) => this.registerConnect(p) });
75
+ const client = new PgTestClient(config, {
76
+ trackConnect: (p) => this.registerConnect(p),
77
+ ...opts
78
+ });
76
79
  this.clients.add(client);
77
80
  const key = this.dbKey(config);
78
81
  this.seenDbConfigs.set(key, config);
@@ -1,11 +1,15 @@
1
1
  import { Client } from 'pg';
2
+ import { getRoleName } from './roles';
2
3
  export class PgTestClient {
3
4
  config;
4
5
  client;
6
+ opts;
5
7
  ctxStmts = '';
8
+ contextSettings = {};
6
9
  _ended = false;
7
10
  connectPromise = null;
8
11
  constructor(config, opts = {}) {
12
+ this.opts = opts;
9
13
  this.config = config;
10
14
  this.client = new Client({
11
15
  host: this.config.host,
@@ -56,12 +60,53 @@ export class PgTestClient {
56
60
  await this.commit();
57
61
  }
58
62
  setContext(ctx) {
59
- this.ctxStmts = Object.entries(ctx)
63
+ Object.assign(this.contextSettings, ctx);
64
+ this.ctxStmts = Object.entries(this.contextSettings)
60
65
  .map(([key, val]) => val === null
61
66
  ? `SELECT set_config('${key}', NULL, true);`
62
67
  : `SELECT set_config('${key}', '${val}', true);`)
63
68
  .join('\n');
64
69
  }
70
+ /**
71
+ * Set authentication context for the current session.
72
+ * Configures role and user ID using cascading defaults from options → opts.auth → RoleMapping.
73
+ */
74
+ auth(options = {}) {
75
+ const role = options.role ?? this.opts.auth?.role ?? getRoleName('authenticated', this.opts);
76
+ const userIdKey = options.userIdKey ?? this.opts.auth?.userIdKey ?? 'jwt.claims.user_id';
77
+ const userId = options.userId ?? this.opts.auth?.userId ?? null;
78
+ this.setContext({
79
+ role,
80
+ [userIdKey]: userId !== null ? String(userId) : null
81
+ });
82
+ }
83
+ /**
84
+ * Commit current transaction to make data visible to other connections, then start fresh transaction.
85
+ * Maintains test isolation by creating a savepoint and reapplying session context.
86
+ */
87
+ async publish() {
88
+ await this.commit(); // make data visible to other sessions
89
+ await this.begin(); // fresh tx
90
+ await this.savepoint(); // keep rollback harness
91
+ await this.ctxQuery(); // reapply all setContext()
92
+ }
93
+ /**
94
+ * Clear all session context variables and reset to default anonymous role.
95
+ */
96
+ clearContext() {
97
+ const defaultRole = getRoleName('anonymous', this.opts);
98
+ const nulledSettings = {};
99
+ Object.keys(this.contextSettings).forEach(key => {
100
+ nulledSettings[key] = null;
101
+ });
102
+ nulledSettings.role = defaultRole;
103
+ this.ctxStmts = Object.entries(nulledSettings)
104
+ .map(([key, val]) => val === null
105
+ ? `SELECT set_config('${key}', NULL, true);`
106
+ : `SELECT set_config('${key}', '${val}', true);`)
107
+ .join('\n');
108
+ this.contextSettings = { role: defaultRole };
109
+ }
65
110
  async any(query, values) {
66
111
  const result = await this.query(query, values);
67
112
  return result.rows;
package/manager.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { Pool } from 'pg';
2
2
  import { PgConfig } from 'pg-env';
3
- import { PgTestClient } from './test-client';
3
+ import { PgTestClient, PgTestClientOpts } from './test-client';
4
4
  export declare class PgTestConnector {
5
5
  private static instance;
6
6
  private readonly clients;
@@ -17,7 +17,7 @@ export declare class PgTestConnector {
17
17
  private registerConnect;
18
18
  private awaitPendingConnects;
19
19
  getPool(config: PgConfig): Pool;
20
- getClient(config: PgConfig): PgTestClient;
20
+ getClient(config: PgConfig, opts?: Partial<PgTestClientOpts>): PgTestClient;
21
21
  closeAll(): Promise<void>;
22
22
  close(): void;
23
23
  drop(config: PgConfig): void;
package/manager.js CHANGED
@@ -71,11 +71,14 @@ class PgTestConnector {
71
71
  }
72
72
  return this.pgPools.get(key);
73
73
  }
74
- getClient(config) {
74
+ getClient(config, opts = {}) {
75
75
  if (this.shuttingDown) {
76
76
  throw new Error('PgTestConnector is shutting down; no new clients allowed');
77
77
  }
78
- const client = new test_client_1.PgTestClient(config, { trackConnect: (p) => this.registerConnect(p) });
78
+ const client = new test_client_1.PgTestClient(config, {
79
+ trackConnect: (p) => this.registerConnect(p),
80
+ ...opts
81
+ });
79
82
  this.clients.add(client);
80
83
  const key = this.dbKey(config);
81
84
  this.seenDbConfigs.set(key, config);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pgsql-test",
3
- "version": "2.10.0",
3
+ "version": "2.11.1",
4
4
  "author": "Dan Lynch <pyramation@gmail.com>",
5
5
  "description": "pgsql-test offers isolated, role-aware, and rollback-friendly PostgreSQL environments for integration tests — giving developers realistic test coverage without external state pollution",
6
6
  "main": "index.js",
@@ -60,14 +60,14 @@
60
60
  "@types/pg-copy-streams": "^1.2.5"
61
61
  },
62
62
  "dependencies": {
63
- "@launchql/core": "^2.11.0",
64
- "@launchql/env": "^2.4.0",
65
- "@launchql/server-utils": "^2.4.0",
66
- "@launchql/types": "^2.5.0",
63
+ "@launchql/core": "^2.11.1",
64
+ "@launchql/env": "^2.4.1",
65
+ "@launchql/server-utils": "^2.4.1",
66
+ "@launchql/types": "^2.6.0",
67
67
  "pg": "^8.16.0",
68
- "pg-cache": "^1.3.0",
68
+ "pg-cache": "^1.3.1",
69
69
  "pg-copy-streams": "^6.0.6",
70
70
  "pg-env": "^1.1.0"
71
71
  },
72
- "gitHead": "f7d2375e14a9f45ebb00d58ab87c0efd6bd50618"
72
+ "gitHead": "720642536863e65eda54f91d69eccebca4ac07d1"
73
73
  }
package/test-client.d.ts CHANGED
@@ -1,13 +1,16 @@
1
1
  import { Client, QueryResult } from 'pg';
2
2
  import { PgConfig } from 'pg-env';
3
- type PgTestClientOpts = {
3
+ import { AuthOptions, PgTestConnectionOptions } from '@launchql/types';
4
+ export type PgTestClientOpts = {
4
5
  deferConnect?: boolean;
5
6
  trackConnect?: (p: Promise<any>) => void;
6
- };
7
+ } & Partial<PgTestConnectionOptions>;
7
8
  export declare class PgTestClient {
8
9
  config: PgConfig;
9
10
  client: Client;
11
+ private opts;
10
12
  private ctxStmts;
13
+ private contextSettings;
11
14
  private _ended;
12
15
  private connectPromise;
13
16
  constructor(config: PgConfig, opts?: PgTestClientOpts);
@@ -20,6 +23,20 @@ export declare class PgTestClient {
20
23
  beforeEach(): Promise<void>;
21
24
  afterEach(): Promise<void>;
22
25
  setContext(ctx: Record<string, string | null>): void;
26
+ /**
27
+ * Set authentication context for the current session.
28
+ * Configures role and user ID using cascading defaults from options → opts.auth → RoleMapping.
29
+ */
30
+ auth(options?: AuthOptions): void;
31
+ /**
32
+ * Commit current transaction to make data visible to other connections, then start fresh transaction.
33
+ * Maintains test isolation by creating a savepoint and reapplying session context.
34
+ */
35
+ publish(): Promise<void>;
36
+ /**
37
+ * Clear all session context variables and reset to default anonymous role.
38
+ */
39
+ clearContext(): void;
23
40
  any<T = any>(query: string, values?: any[]): Promise<T[]>;
24
41
  one<T = any>(query: string, values?: any[]): Promise<T>;
25
42
  oneOrNone<T = any>(query: string, values?: any[]): Promise<T | null>;
@@ -30,4 +47,3 @@ export declare class PgTestClient {
30
47
  query<T = any>(query: string, values?: any[]): Promise<QueryResult<T>>;
31
48
  ctxQuery(): Promise<void>;
32
49
  }
33
- export {};
package/test-client.js CHANGED
@@ -2,13 +2,17 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.PgTestClient = void 0;
4
4
  const pg_1 = require("pg");
5
+ const roles_1 = require("./roles");
5
6
  class PgTestClient {
6
7
  config;
7
8
  client;
9
+ opts;
8
10
  ctxStmts = '';
11
+ contextSettings = {};
9
12
  _ended = false;
10
13
  connectPromise = null;
11
14
  constructor(config, opts = {}) {
15
+ this.opts = opts;
12
16
  this.config = config;
13
17
  this.client = new pg_1.Client({
14
18
  host: this.config.host,
@@ -59,12 +63,53 @@ class PgTestClient {
59
63
  await this.commit();
60
64
  }
61
65
  setContext(ctx) {
62
- this.ctxStmts = Object.entries(ctx)
66
+ Object.assign(this.contextSettings, ctx);
67
+ this.ctxStmts = Object.entries(this.contextSettings)
63
68
  .map(([key, val]) => val === null
64
69
  ? `SELECT set_config('${key}', NULL, true);`
65
70
  : `SELECT set_config('${key}', '${val}', true);`)
66
71
  .join('\n');
67
72
  }
73
+ /**
74
+ * Set authentication context for the current session.
75
+ * Configures role and user ID using cascading defaults from options → opts.auth → RoleMapping.
76
+ */
77
+ auth(options = {}) {
78
+ const role = options.role ?? this.opts.auth?.role ?? (0, roles_1.getRoleName)('authenticated', this.opts);
79
+ const userIdKey = options.userIdKey ?? this.opts.auth?.userIdKey ?? 'jwt.claims.user_id';
80
+ const userId = options.userId ?? this.opts.auth?.userId ?? null;
81
+ this.setContext({
82
+ role,
83
+ [userIdKey]: userId !== null ? String(userId) : null
84
+ });
85
+ }
86
+ /**
87
+ * Commit current transaction to make data visible to other connections, then start fresh transaction.
88
+ * Maintains test isolation by creating a savepoint and reapplying session context.
89
+ */
90
+ async publish() {
91
+ await this.commit(); // make data visible to other sessions
92
+ await this.begin(); // fresh tx
93
+ await this.savepoint(); // keep rollback harness
94
+ await this.ctxQuery(); // reapply all setContext()
95
+ }
96
+ /**
97
+ * Clear all session context variables and reset to default anonymous role.
98
+ */
99
+ clearContext() {
100
+ const defaultRole = (0, roles_1.getRoleName)('anonymous', this.opts);
101
+ const nulledSettings = {};
102
+ Object.keys(this.contextSettings).forEach(key => {
103
+ nulledSettings[key] = null;
104
+ });
105
+ nulledSettings.role = defaultRole;
106
+ this.ctxStmts = Object.entries(nulledSettings)
107
+ .map(([key, val]) => val === null
108
+ ? `SELECT set_config('${key}', NULL, true);`
109
+ : `SELECT set_config('${key}', '${val}', true);`)
110
+ .join('\n');
111
+ this.contextSettings = { role: defaultRole };
112
+ }
68
113
  async any(query, values) {
69
114
  const result = await this.query(query, values);
70
115
  return result.rows;