constructive-test 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,23 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2025 Dan Lynch <pyramation@gmail.com>
4
+ Copyright (c) 2025 Constructive <developers@constructive.io>
5
+ Copyright (c) 2020-present, Interweb, Inc.
6
+
7
+ Permission is hereby granted, free of charge, to any person obtaining a copy
8
+ of this software and associated documentation files (the "Software"), to deal
9
+ in the Software without restriction, including without limitation the rights
10
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11
+ copies of the Software, and to permit persons to whom the Software is
12
+ furnished to do so, subject to the following conditions:
13
+
14
+ The above copyright notice and this permission notice shall be included in all
15
+ copies or substantial portions of the Software.
16
+
17
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,728 @@
1
+ # constructive-test
2
+
3
+ <p align="center" width="100%">
4
+ <img height="250" src="https://raw.githubusercontent.com/constructive-io/constructive/refs/heads/main/assets/outline-logo.svg" />
5
+ </p>
6
+
7
+ <p align="center" width="100%">
8
+ <a href="https://github.com/constructive-io/constructive/actions/workflows/run-tests.yaml">
9
+ <img height="20" src="https://github.com/constructive-io/constructive/actions/workflows/run-tests.yaml/badge.svg" />
10
+ </a>
11
+ <a href="https://github.com/constructive-io/constructive/blob/main/LICENSE">
12
+ <img height="20" src="https://img.shields.io/badge/license-MIT-blue.svg"/>
13
+ </a>
14
+ <a href="https://www.npmjs.com/package/constructive-test">
15
+ <img height="20" src="https://img.shields.io/github/package-json/v/constructive-io/constructive?filename=postgres%2Fconstructive-test%2Fpackage.json"/>
16
+ </a>
17
+ </p>
18
+
19
+ `constructive-test` is a Constructive-optimized version of [`pgsql-test`](https://www.npmjs.com/package/pgsql-test) with Constructive platform conventions baked in. It provides instant, isolated PostgreSQL databases for testing with automatic transaction rollbacks, context switching, and clean seeding โ€” configured for the Constructive platform's principal-based identity model.
20
+
21
+ ## Install
22
+
23
+ ```sh
24
+ npm install constructive-test
25
+ ```
26
+
27
+ ## Features
28
+
29
+ * โšก **Instant test DBs** โ€” each one seeded, isolated, and UUID-named
30
+ * ๐Ÿ”„ **Per-test rollback** โ€” every test runs in its own transaction or savepoint
31
+ * ๐Ÿ›ก **RLS-friendly** โ€” test with role-based auth via `.setContext()`
32
+ * ๐Ÿ‘ค **Principal-aware** โ€” automatically derives `principal_id` from `user_id` for testing principals (agents, API keys, humans, etc.)
33
+ * ๐ŸŒฑ **Flexible seeding** โ€” run `.sql` files, programmatic seeds, or even load fixtures
34
+ * ๐Ÿงช **Compatible with any async runner** โ€” works with `Jest`, `Mocha`, etc.
35
+ * ๐Ÿงน **Auto teardown** โ€” no residue, no reboots, just clean exits
36
+
37
+ ### pgpm migrations
38
+
39
+ Part of the [pgpm](https://pgpm.io) ecosystem, `constructive-test` is built to pair seamlessly with our TypeScript-based package manager and migration tool. `pgpm` gives you modular Postgres packages, deterministic plans, and tag-aware releases โ€” perfect for authoring the migrations that `constructive-test` runs.
40
+
41
+ ## Table of Contents
42
+
43
+ 1. [Install](#install)
44
+ 2. [Features](#features)
45
+ 3. [Quick Start](#-quick-start)
46
+ 4. [Principal Identity](#-principal-identity)
47
+ 5. [`getConnections()` Overview](#getconnections-overview)
48
+ 6. [PgTestClient API Overview](#pgtestclient-api-overview)
49
+ 7. [Usage Examples](#usage-examples)
50
+ * [Basic Setup](#-basic-setup)
51
+ * [Role-Based Context](#-role-based-context)
52
+ * [Seeding System](#-seeding-system)
53
+ * [SQL File Seeding](#-sql-file-seeding)
54
+ * [Programmatic Seeding](#-programmatic-seeding)
55
+ * [CSV Seeding](#-csv-seeding)
56
+ * [JSON Seeding](#-json-seeding)
57
+ * [pgpm Seeding](#-pgpm-seeding)
58
+ 8. [`getConnections() Options`](#getconnections-options)
59
+ 9. [Disclaimer](#disclaimer)
60
+
61
+
62
+ ## โœจ Quick Start
63
+
64
+ ```ts
65
+ import { getConnections } from 'constructive-test';
66
+
67
+ let db, teardown;
68
+
69
+ beforeAll(async () => {
70
+ ({ db, teardown } = await getConnections());
71
+ await db.query(`SELECT 1`); // โœ… Ready to run queries
72
+ });
73
+
74
+ afterAll(() => teardown());
75
+ ```
76
+
77
+ ## ๐Ÿ‘ค Principal Identity
78
+
79
+ The Constructive platform uses a principal-based identity model where every actor โ€” whether a human user, an API key, or an agent โ€” is represented as a principal. RLS policies enforce access based on `jwt.claims.principal_id`.
80
+
81
+ `constructive-test` handles this automatically: when you call `setContext()` with a `user_id`, the `principal_id` is derived for you. No manual setup, no boilerplate.
82
+
83
+ ```ts
84
+ db.setContext({
85
+ role: 'authenticated',
86
+ 'jwt.claims.user_id': '123',
87
+ 'jwt.claims.database_id': 'abc'
88
+ // jwt.claims.principal_id is automatically set to '123'
89
+ });
90
+ ```
91
+
92
+ When testing non-human principals (API keys, agents), set `principal_id` explicitly:
93
+
94
+ ```ts
95
+ db.setContext({
96
+ role: 'authenticated',
97
+ 'jwt.claims.user_id': '123',
98
+ 'jwt.claims.principal_id': 'agent-456',
99
+ 'jwt.claims.database_id': 'abc'
100
+ });
101
+ ```
102
+
103
+ ## `getConnections()` Overview
104
+
105
+ ```ts
106
+ import { getConnections } from 'constructive-test';
107
+
108
+ // Complete object destructuring
109
+ const { pg, db, admin, teardown, manager } = await getConnections();
110
+
111
+ // Most common pattern
112
+ const { db, teardown } = await getConnections();
113
+ ```
114
+
115
+ The `getConnections()` helper sets up a fresh PostgreSQL test database and returns a structured object with:
116
+
117
+ * `pg`: a `PgTestClient` connected as the root or superuser โ€” useful for administrative setup or introspection
118
+ * `db`: a `PgTestClient` connected as the app-level user โ€” used for running tests with RLS and granted permissions
119
+ * `admin`: a `DbAdmin` utility for managing database state, extensions, roles, and templates
120
+ * `teardown()`: a function that shuts down the test environment and database pool
121
+ * `manager`: a shared connection pool manager (`PgTestConnector`) behind both clients
122
+
123
+ Together, these allow fast, isolated, role-aware test environments with per-test rollback and full control over setup and teardown.
124
+
125
+ The `PgTestClient` returned by `getConnections()` is a fully-featured wrapper around `pg.Pool`. It provides:
126
+
127
+ * Automatic transaction and savepoint management for test isolation
128
+ * Easy switching of role-based contexts for RLS testing
129
+ * A clean, high-level API for integration testing PostgreSQL systems
130
+
131
+ ## `PgTestClient` API Overview
132
+
133
+ ```ts
134
+ let pg: PgTestClient;
135
+ let teardown: () => Promise<void>;
136
+
137
+ beforeAll(async () => {
138
+ ({ pg, teardown } = await getConnections());
139
+ });
140
+
141
+ beforeEach(() => pg.beforeEach());
142
+ afterEach(() => pg.afterEach());
143
+ afterAll(() => teardown());
144
+ ```
145
+
146
+ The `PgTestClient` returned by `getConnections()` wraps a `pg.Client` and provides convenient helpers for query execution, test isolation, and context switching.
147
+
148
+ ### Common Methods
149
+
150
+ * `query(sql, values?)` โ€“ Run a raw SQL query and get the `QueryResult`
151
+ * `beforeEach()` โ€“ Begins a transaction and sets a savepoint (called at the start of each test)
152
+ * `afterEach()` โ€“ Rolls back to the savepoint and commits the outer transaction (cleans up test state)
153
+ * `setContext({ key: value })` โ€“ Sets PostgreSQL config variables (like `role`) to simulate RLS contexts
154
+ * `any`, `one`, `oneOrNone`, `many`, `manyOrNone`, `none`, `result` โ€“ Typed query helpers for specific result expectations
155
+
156
+ These methods make it easier to build expressive and isolated integration tests with strong typing and error handling.
157
+
158
+ ## Usage Examples
159
+
160
+ ### โšก Basic Setup
161
+
162
+ ```ts
163
+ import { getConnections } from 'constructive-test';
164
+
165
+ let db; // A fully wrapped PgTestClient using pg.Pool with savepoint-based rollback per test
166
+ let teardown;
167
+
168
+ beforeAll(async () => {
169
+ ({ db, teardown } = await getConnections());
170
+
171
+ await db.query(`
172
+ CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT);
173
+ CREATE TABLE posts (id SERIAL PRIMARY KEY, user_id INT REFERENCES users(id), content TEXT);
174
+
175
+ INSERT INTO users (name) VALUES ('Alice'), ('Bob');
176
+ INSERT INTO posts (user_id, content) VALUES (1, 'Hello world!'), (2, 'Graphile is cool!');
177
+ `);
178
+ });
179
+
180
+ afterAll(() => teardown());
181
+
182
+ beforeEach(() => db.beforeEach());
183
+ afterEach(() => db.afterEach());
184
+
185
+ test('user count starts at 2', async () => {
186
+ const res = await db.query('SELECT COUNT(*) FROM users');
187
+ expect(res.rows[0].count).toBe('2');
188
+ });
189
+ ```
190
+
191
+ ### ๐Ÿ” Role-Based Context
192
+
193
+
194
+ The `constructive-test` framework provides powerful tools to simulate authentication contexts during tests, which is particularly useful when testing Row-Level Security (RLS) policies.
195
+
196
+ #### Setting Test Context
197
+
198
+ Use `setContext()` to simulate different user roles and JWT claims:
199
+
200
+ ```ts
201
+ db.setContext({
202
+ role: 'authenticated',
203
+ 'jwt.claims.user_id': '123',
204
+ 'jwt.claims.org_id': 'acme'
205
+ });
206
+ ```
207
+
208
+ This applies the settings using `SET LOCAL` statements, ensuring they persist only for the current transaction and maintain proper isolation between tests.
209
+
210
+ #### Testing Role-Based Access
211
+
212
+ ```ts
213
+ describe('authenticated role', () => {
214
+ beforeEach(async () => {
215
+ db.setContext({ role: 'authenticated' });
216
+ await db.beforeEach();
217
+ });
218
+
219
+ afterEach(() => db.afterEach());
220
+
221
+ it('runs as authenticated', async () => {
222
+ const res = await db.query(`SELECT current_setting('role', true) AS role`);
223
+ expect(res.rows[0].role).toBe('authenticated');
224
+ });
225
+ });
226
+ ```
227
+
228
+ #### Database Connection Options
229
+
230
+ For non-superuser testing, use the connection options described in the [options](#getconnections-options) section. The `db.connection` property allows you to customize the non-privileged user account for your tests.
231
+
232
+ Use `setContext()` to simulate Role-Based Access Control (RBAC) during tests. This is useful when testing Row-Level Security (RLS) policies. Your actual server should manage role/user claims via secure tokens (e.g., setting `current_setting('jwt.claims.user_id')`), but this interface helps emulate those behaviors in test environments.
233
+
234
+ #### Common Testing Scenarios
235
+
236
+ This approach enables testing various access patterns:
237
+ - Authenticated vs. anonymous user access
238
+ - Per-user data filtering
239
+ - Admin privilege bypass behavior
240
+ - Custom claim-based restrictions (organization membership, admin status)
241
+ - Principal-based access control (agents, API keys, human users)
242
+
243
+ > **Note:** While this interface helps simulate RBAC for testing, your production server should manage user/role claims via secure authentication tokens, typically by setting values like `current_setting('jwt.claims.user_id')` through proper authentication middleware.
244
+
245
+ ### ๐ŸŒฑ Seeding System
246
+
247
+ The second argument to `getConnections()` is an optional array of `SeedAdapter` objects:
248
+
249
+ ```ts
250
+ const { db, teardown } = await getConnections(getConnectionOptions, seedAdapters);
251
+ ```
252
+
253
+ This array lets you fully customize how your test database is seeded. You can compose multiple strategies:
254
+
255
+ * [`seed.sqlfile()`](#-sql-file-seeding) โ€“ Execute raw `.sql` files from disk
256
+ * [`seed.fn()`](#-programmatic-seeding) โ€“ Run JavaScript/TypeScript logic to programmatically insert data
257
+ * [`seed.csv()`](#-csv-seeding) โ€“ Load tabular data from CSV files
258
+ * [`seed.json()`](#-json-seeding) โ€“ Use in-memory objects as seed data
259
+ * [`seed.pgpm()`](#-pgpm-seeding) โ€“ Apply a PGPM project or set of packages (compatible with sqitch)
260
+
261
+ > โœจ **Default Behavior:** If no `SeedAdapter[]` is passed, pgpm seeding is assumed. This makes `constructive-test` zero-config for pgpm-based projects.
262
+
263
+ This composable system allows you to mix-and-match data setup strategies for flexible, realistic, and fast database tests.
264
+
265
+ #### Two Seeding Patterns
266
+
267
+ You can seed data using either approach:
268
+
269
+ **1. Adapter Pattern** (setup phase via `getConnections`)
270
+ ```ts
271
+ const { db, teardown } = await getConnections({}, [
272
+ seed.json({ 'users': [{ id: 1, name: 'Alice' }] })
273
+ ]);
274
+ ```
275
+
276
+ **2. Direct Load Methods** (runtime via `PgTestClient`)
277
+ ```ts
278
+ await db.loadJson({ 'users': [{ id: 1, name: 'Alice' }] });
279
+ await db.loadCsv({ 'users': '/path/to/users.csv' });
280
+ await db.loadSql(['/path/to/schema.sql']);
281
+ ```
282
+
283
+ > **Note:** `loadCsv()` and `loadPgpm()` do not apply RLS context (PostgreSQL limitation). Use `loadJson()` or `loadSql()` for RLS-aware seeding.
284
+
285
+ ### ๐Ÿ”Œ SQL File Seeding
286
+
287
+ **Adapter Pattern:**
288
+ ```ts
289
+ const { db, teardown } = await getConnections({}, [
290
+ seed.sqlfile(['schema.sql', 'fixtures.sql'])
291
+ ]);
292
+ ```
293
+
294
+ **Direct Load Method:**
295
+ ```ts
296
+ await db.loadSql(['schema.sql', 'fixtures.sql']);
297
+ ```
298
+
299
+ <details>
300
+ <summary>Full example</summary>
301
+
302
+ ```ts
303
+ import path from 'path';
304
+ import { getConnections, seed } from 'constructive-test';
305
+
306
+ const sql = (f: string) => path.join(__dirname, 'sql', f);
307
+
308
+ let db;
309
+ let teardown;
310
+
311
+ beforeAll(async () => {
312
+ ({ db, teardown } = await getConnections({}, [
313
+ seed.sqlfile([
314
+ sql('schema.sql'),
315
+ sql('fixtures.sql')
316
+ ])
317
+ ]));
318
+ });
319
+
320
+ afterAll(async () => {
321
+ await teardown();
322
+ });
323
+ ```
324
+
325
+ </details>
326
+
327
+ ### ๐Ÿง  Programmatic Seeding
328
+
329
+ **Adapter Pattern:**
330
+ ```ts
331
+ const { db, teardown } = await getConnections({}, [
332
+ seed.fn(async ({ pg }) => {
333
+ await pg.query(`INSERT INTO users (name) VALUES ('Seeded User')`);
334
+ })
335
+ ]);
336
+ ```
337
+
338
+ **Direct Load Method:**
339
+ ```ts
340
+ // Use any PgTestClient method directly
341
+ await db.query(`INSERT INTO users (name) VALUES ('Seeded User')`);
342
+ ```
343
+
344
+ <details>
345
+ <summary>Full example</summary>
346
+
347
+ ```ts
348
+ import { getConnections, seed } from 'constructive-test';
349
+
350
+ let db;
351
+ let teardown;
352
+
353
+ beforeAll(async () => {
354
+ ({ db, teardown } = await getConnections({}, [
355
+ seed.fn(async ({ pg }) => {
356
+ await pg.query(`
357
+ INSERT INTO users (name) VALUES ('Seeded User');
358
+ `);
359
+ })
360
+ ]));
361
+ });
362
+ ```
363
+
364
+ </details>
365
+
366
+ ## ๐Ÿ—ƒ CSV Seeding
367
+
368
+ **Adapter Pattern:**
369
+ ```ts
370
+ const { db, teardown } = await getConnections({}, [
371
+ seed.csv({
372
+ 'users': '/path/to/users.csv',
373
+ 'posts': '/path/to/posts.csv'
374
+ })
375
+ ]);
376
+ ```
377
+
378
+ **Direct Load Method:**
379
+ ```ts
380
+ await db.loadCsv({
381
+ 'users': '/path/to/users.csv',
382
+ 'posts': '/path/to/posts.csv'
383
+ });
384
+ ```
385
+
386
+ > **Note:** CSV loading uses PostgreSQL COPY which does not support RLS context.
387
+
388
+ <details>
389
+ <summary>Full example</summary>
390
+
391
+ You can load tables from CSV files using `seed.csv({ ... })`. CSV headers must match the table column names exactly. This is useful for loading stable fixture data for integration tests or CI environments.
392
+
393
+ ```ts
394
+ import path from 'path';
395
+ import { getConnections, seed } from 'constructive-test';
396
+
397
+ const csv = (file: string) => path.resolve(__dirname, '../csv', file);
398
+
399
+ let db;
400
+ let teardown;
401
+
402
+ beforeAll(async () => {
403
+ ({ db, teardown } = await getConnections({}, [
404
+ // Create schema
405
+ seed.fn(async ({ pg }) => {
406
+ await pg.query(`
407
+ CREATE TABLE users (
408
+ id SERIAL PRIMARY KEY,
409
+ name TEXT NOT NULL
410
+ );
411
+
412
+ CREATE TABLE posts (
413
+ id SERIAL PRIMARY KEY,
414
+ user_id INT REFERENCES users(id),
415
+ content TEXT NOT NULL
416
+ );
417
+ `);
418
+ }),
419
+ // Load from CSV
420
+ seed.csv({
421
+ users: csv('users.csv'),
422
+ posts: csv('posts.csv')
423
+ }),
424
+ // Adjust SERIAL sequences to avoid conflicts
425
+ seed.fn(async ({ pg }) => {
426
+ await pg.query(`SELECT setval(pg_get_serial_sequence('users', 'id'), (SELECT MAX(id) FROM users));`);
427
+ await pg.query(`SELECT setval(pg_get_serial_sequence('posts', 'id'), (SELECT MAX(id) FROM posts));`);
428
+ })
429
+ ]));
430
+ });
431
+
432
+ afterAll(() => teardown());
433
+
434
+ it('has loaded rows', async () => {
435
+ const res = await db.query('SELECT COUNT(*) FROM users');
436
+ expect(+res.rows[0].count).toBeGreaterThan(0);
437
+ });
438
+ ```
439
+
440
+ </details>
441
+
442
+ ## ๐Ÿ—ƒ JSON Seeding
443
+
444
+ **Adapter Pattern:**
445
+ ```ts
446
+ const { db, teardown } = await getConnections({}, [
447
+ seed.json({
448
+ 'custom.users': [
449
+ { id: 1, name: 'Alice' },
450
+ { id: 2, name: 'Bob' }
451
+ ]
452
+ })
453
+ ]);
454
+ ```
455
+
456
+ **Direct Load Method:**
457
+ ```ts
458
+ await db.loadJson({
459
+ 'custom.users': [
460
+ { id: 1, name: 'Alice' },
461
+ { id: 2, name: 'Bob' }
462
+ ]
463
+ });
464
+ ```
465
+
466
+ <details>
467
+ <summary>Full example</summary>
468
+
469
+ You can seed tables using in-memory JSON objects. This is useful when you want fast, inline fixtures without managing external files.
470
+
471
+ ```ts
472
+ import { getConnections, seed } from 'constructive-test';
473
+
474
+ let db;
475
+ let teardown;
476
+
477
+ beforeAll(async () => {
478
+ ({ db, teardown } = await getConnections({}, [
479
+ // Create schema
480
+ seed.fn(async ({ pg }) => {
481
+ await pg.query(`
482
+ CREATE SCHEMA custom;
483
+ CREATE TABLE custom.users (
484
+ id SERIAL PRIMARY KEY,
485
+ name TEXT NOT NULL
486
+ );
487
+
488
+ CREATE TABLE custom.posts (
489
+ id SERIAL PRIMARY KEY,
490
+ user_id INT REFERENCES custom.users(id),
491
+ content TEXT NOT NULL
492
+ );
493
+ `);
494
+ }),
495
+ // Seed with in-memory JSON
496
+ seed.json({
497
+ 'custom.users': [
498
+ { id: 1, name: 'Alice' },
499
+ { id: 2, name: 'Bob' }
500
+ ],
501
+ 'custom.posts': [
502
+ { id: 1, user_id: 1, content: 'Hello world!' },
503
+ { id: 2, user_id: 2, content: 'Graphile is cool!' }
504
+ ]
505
+ }),
506
+ // Fix SERIAL sequences
507
+ seed.fn(async ({ pg }) => {
508
+ await pg.query(`SELECT setval(pg_get_serial_sequence('custom.users', 'id'), (SELECT MAX(id) FROM custom.users));`);
509
+ await pg.query(`SELECT setval(pg_get_serial_sequence('custom.posts', 'id'), (SELECT MAX(id) FROM custom.posts));`);
510
+ })
511
+ ]));
512
+ });
513
+
514
+ afterAll(() => teardown());
515
+
516
+ it('has loaded rows', async () => {
517
+ const res = await db.query('SELECT COUNT(*) FROM custom.users');
518
+ expect(+res.rows[0].count).toBeGreaterThan(0);
519
+ });
520
+ ```
521
+
522
+ </details>
523
+
524
+ ## ๐Ÿš€ pgpm Seeding
525
+
526
+ **Zero Configuration (Default):**
527
+ ```ts
528
+ // pgpm migrate is used automatically
529
+ const { db, teardown } = await getConnections();
530
+ ```
531
+
532
+ **Adapter Pattern (Custom Path):**
533
+ ```ts
534
+ const { db, teardown } = await getConnections({}, [
535
+ seed.pgpm('/path/to/your-pgpm-workspace', true) // with cache
536
+ ]);
537
+ ```
538
+
539
+ **Direct Load Method:**
540
+ ```ts
541
+ await db.loadPgpm('/path/to/your-pgpm-workspace', true); // with cache
542
+ ```
543
+
544
+ > **Note:** pgpm deployment has its own client handling and does not apply RLS context.
545
+
546
+ <details>
547
+ <summary>Full example</summary>
548
+
549
+ If your project uses pgpm modules with a precompiled `pgpm.plan`, you can use `constructive-test` with **zero configuration**. Just call `getConnections()` โ€” and it *just works*:
550
+
551
+ ```ts
552
+ import { getConnections } from 'constructive-test';
553
+
554
+ let db, teardown;
555
+
556
+ beforeAll(async () => {
557
+ ({ db, teardown } = await getConnections()); // pgpm module is deployed automatically
558
+ });
559
+ ```
560
+
561
+ pgpm uses Sqitch-compatible syntax with a TypeScript-based migration engine. By default, `constructive-test` automatically deploys any pgpm module found in the current working directory (`process.cwd()`).
562
+
563
+ To specify a custom path to your pgpm module, use `seed.pgpm()` explicitly:
564
+
565
+ ```ts
566
+ import path from 'path';
567
+ import { getConnections, seed } from 'constructive-test';
568
+
569
+ const cwd = path.resolve(__dirname, '../path/to/pgpm-workspace');
570
+
571
+ beforeAll(async () => {
572
+ ({ db, teardown } = await getConnections({}, [
573
+ seed.pgpm(cwd)
574
+ ]));
575
+ });
576
+ ```
577
+
578
+ </details>
579
+
580
+ ## Why PGPM's Approach?
581
+
582
+ pgpm provides the best of both worlds:
583
+
584
+ 1. **Sqitch Compatibility**: Keep your familiar Sqitch syntax and migration approach
585
+ 2. **TypeScript Performance**: Our TS-rewritten deployment engine delivers up to 10x faster schema deployments
586
+ 3. **Developer Experience**: Tight feedback loops with near-instant schema setup for tests
587
+ 4. **CI Optimization**: Dramatically reduced test suite run times with optimized deployment
588
+
589
+ By maintaining Sqitch compatibility while supercharging performance, pgpm enables you to keep your existing migration patterns while enjoying the speed benefits of our TypeScript engine.
590
+
591
+ ## `getConnections` Options
592
+
593
+ This table documents the available options for the `getConnections` function. The options are passed as a combination of `pg` and `db` configuration objects.
594
+
595
+ ### `db` Options (PgTestConnectionOptions)
596
+
597
+ | Option | Type | Default | Description |
598
+ | ------------------------ | ---------- | ---------------- | ------------------------------------------------------------------- |
599
+ | `db.extensions` | `string[]` | `[]` | Array of PostgreSQL extensions to include in the test database |
600
+ | `db.cwd` | `string` | `process.cwd()` | Working directory used for PGPM or Sqitch projects |
601
+ | `db.connection.user` | `string` | `'app_user'` | User for simulating RLS via `setContext()` |
602
+ | `db.connection.password` | `string` | `'app_password'` | Password for RLS test user |
603
+ | `db.connection.role` | `string` | `'anonymous'` | Default role used during `setContext()` |
604
+ | `db.template` | `string` | `undefined` | Template database used for faster test DB creation |
605
+ | `db.rootDb` | `string` | `'postgres'` | Root database used for administrative operations (e.g., creating databases) |
606
+ | `db.prefix` | `string` | `'db-'` | Prefix used when generating test database names |
607
+
608
+ ### `pg` Options (PgConfig)
609
+
610
+ Environment variables will override these options when available:
611
+
612
+ * `PGHOST`, `PGPORT`, `PGUSER`, `PGPASSWORD`, `PGDATABASE`
613
+
614
+ | Option | Type | Default | Description |
615
+ | ------------- | -------- | ------------- | ---------------------------------------------- |
616
+ | `pg.user` | `string` | `'postgres'` | Superuser for PostgreSQL |
617
+ | `pg.password` | `string` | `'password'` | Password for the PostgreSQL superuser |
618
+ | `pg.host` | `string` | `'localhost'` | Hostname for PostgreSQL |
619
+ | `pg.port` | `number` | `5432` | Port for PostgreSQL |
620
+ | `pg.database` | `string` | `'postgres'` | Default database used when connecting initially |
621
+
622
+ ### Usage
623
+
624
+ ```ts
625
+ const { conn, db, teardown } = await getConnections({
626
+ pg: { user: 'postgres', password: 'secret' },
627
+ db: {
628
+ extensions: ['uuid-ossp'],
629
+ cwd: '/path/to/project',
630
+ connection: { user: 'test_user', password: 'secret', role: 'authenticated' },
631
+ template: 'test_template',
632
+ prefix: 'test_',
633
+ rootDb: 'postgres'
634
+ }
635
+ });
636
+ ```
637
+
638
+ ## Snapshot Utilities
639
+
640
+ The `constructive-test/utils` module provides utilities for sanitizing query results for snapshot testing. These helpers replace dynamic values (IDs, UUIDs, dates, hashes) with stable placeholders, making snapshots deterministic.
641
+
642
+ ```ts
643
+ import { snapshot } from 'constructive-test/utils';
644
+
645
+ const result = await db.any('SELECT * FROM users');
646
+ expect(snapshot(result)).toMatchSnapshot();
647
+ ```
648
+
649
+ See [`pgsql-test` Snapshot Utilities](https://www.npmjs.com/package/pgsql-test#snapshot-utilities) for the full API reference.
650
+
651
+ ## Disclaimer
652
+
653
+ AS DESCRIBED IN THE LICENSES, THE SOFTWARE IS PROVIDED "AS IS", AT YOUR OWN RISK, AND WITHOUT WARRANTIES OF ANY KIND.
654
+
655
+ No developer or entity involved in creating this software will be liable for any claims or damages whatsoever associated with your use, inability to use, or your interaction with other users of the code, including any direct, indirect, incidental, special, exemplary, punitive or consequential damages, or loss of profits, cryptocurrencies, tokens, or anything else of value.
656
+
657
+ ---
658
+
659
+ ## Education and Tutorials
660
+
661
+ 1. ๐Ÿš€ [Quickstart: Getting Up and Running](https://constructive.io/learn/quickstart)
662
+ Get started with modular databases in minutes. Install prerequisites and deploy your first module.
663
+
664
+ 2. ๐Ÿ“ฆ [Modular PostgreSQL Development with Database Packages](https://constructive.io/learn/modular-postgres)
665
+ Learn to organize PostgreSQL projects with pgpm workspaces and reusable database modules.
666
+
667
+ 3. โœ๏ธ [Authoring Database Changes](https://constructive.io/learn/authoring-database-changes)
668
+ Master the workflow for adding, organizing, and managing database changes with pgpm.
669
+
670
+ 4. ๐Ÿงช [End-to-End PostgreSQL Testing with TypeScript](https://constructive.io/learn/e2e-postgres-testing)
671
+ Master end-to-end PostgreSQL testing with ephemeral databases, RLS testing, and CI/CD automation.
672
+
673
+ 5. โšก [Supabase Testing](https://constructive.io/learn/supabase)
674
+ Use TypeScript-first tools to test Supabase projects with realistic RLS, policies, and auth contexts.
675
+
676
+ 6. ๐Ÿ’ง [Drizzle ORM Testing](https://constructive.io/learn/drizzle-testing)
677
+ Run full-stack tests with Drizzle ORM, including database setup, teardown, and RLS enforcement.
678
+
679
+ 7. ๐Ÿ”ง [Troubleshooting](https://constructive.io/learn/troubleshooting)
680
+ Common issues and solutions for pgpm, PostgreSQL, and testing.
681
+
682
+ ## Related Constructive Tooling
683
+
684
+ ### ๐Ÿ“ฆ Package Management
685
+
686
+ * [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.
687
+
688
+ ### ๐Ÿงช Testing
689
+
690
+ * [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.
691
+ * [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.
692
+ * [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.
693
+ * [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.
694
+ * [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.
695
+
696
+ ### ๐Ÿง  Parsing & AST
697
+
698
+ * [pgsql-parser](https://www.npmjs.com/package/pgsql-parser): **๐Ÿ”„ SQL conversion engine** that interprets and converts PostgreSQL syntax.
699
+ * [libpg-query-node](https://www.npmjs.com/package/libpg-query): **๐ŸŒ‰ Node.js bindings** for `libpg_query`, converting SQL into parse trees.
700
+ * [pg-proto-parser](https://www.npmjs.com/package/pg-proto-parser): **๐Ÿ“ฆ Protobuf parser** for parsing PostgreSQL Protocol Buffers definitions to generate TypeScript interfaces, utility functions, and JSON mappings for enums.
701
+ * [@pgsql/enums](https://www.npmjs.com/package/@pgsql/enums): **๐Ÿท๏ธ TypeScript enums** for PostgreSQL AST for safe and ergonomic parsing logic.
702
+ * [@pgsql/types](https://www.npmjs.com/package/@pgsql/types): **๐Ÿ“ Type definitions** for PostgreSQL AST nodes in TypeScript.
703
+ * [@pgsql/utils](https://www.npmjs.com/package/@pgsql/utils): **๐Ÿ› ๏ธ AST utilities** for constructing and transforming PostgreSQL syntax trees.
704
+
705
+ ### ๐Ÿ“š Documentation & Skills
706
+
707
+ * [constructive-skills](https://github.com/constructive-io/constructive-skills): **๐Ÿ“– Platform documentation and AI agent skills** โ€” feature catalog, blueprint reference, SDK guides (i18n, billing, limits, events, uploads, security, entities, search, AI), and deployment guides.
708
+
709
+ Install skills for AI coding agents:
710
+
711
+ ```bash
712
+ # All platform skills (security, blueprints, codegen, billing, etc.)
713
+ npx skills add constructive-io/constructive-skills
714
+
715
+ # Individual repo skills (pgpm, testing, CLI, search, etc.)
716
+ npx skills add https://github.com/constructive-io/constructive --skill pgpm
717
+ npx skills add https://github.com/constructive-io/constructive --skill constructive-testing
718
+ ```
719
+
720
+ ## Credits
721
+
722
+ **๐Ÿ›  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).**
723
+
724
+ ## Disclaimer
725
+
726
+ AS DESCRIBED IN THE LICENSES, THE SOFTWARE IS PROVIDED "AS IS", AT YOUR OWN RISK, AND WITHOUT WARRANTIES OF ANY KIND.
727
+
728
+ No developer or entity involved in creating this software will be liable for any claims or damages whatsoever associated with your use, inability to use, or your interaction with other users of the code, including any direct, indirect, incidental, special, exemplary, punitive or consequential damages, or loss of profits, cryptocurrencies, tokens, or anything else of value.
package/connect.d.ts ADDED
@@ -0,0 +1,12 @@
1
+ import { getConnections as getPgConnections, type GetConnectionOpts, type GetConnectionResult } from 'pgsql-test';
2
+ /**
3
+ * Get connections with Constructive platform behavior.
4
+ *
5
+ * Delegates entirely to pgsql-test's getConnections (which already
6
+ * resolves pgpmDefaults + config file + env vars + overrides via
7
+ * getConnEnvOptions), then wraps the returned `db` client so that
8
+ * every `setContext()` call auto-derives `jwt.claims.principal_id`
9
+ * from `jwt.claims.user_id` when the caller hasn't set it explicitly.
10
+ */
11
+ export declare const getConnections: (cn?: GetConnectionOpts, seedAdapters?: Parameters<typeof getPgConnections>[1]) => Promise<GetConnectionResult>;
12
+ export type { GetConnectionOpts, GetConnectionResult };
package/connect.js ADDED
@@ -0,0 +1,26 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getConnections = void 0;
4
+ const pgsql_test_1 = require("pgsql-test");
5
+ /**
6
+ * Get connections with Constructive platform behavior.
7
+ *
8
+ * Delegates entirely to pgsql-test's getConnections (which already
9
+ * resolves pgpmDefaults + config file + env vars + overrides via
10
+ * getConnEnvOptions), then wraps the returned `db` client so that
11
+ * every `setContext()` call auto-derives `jwt.claims.principal_id`
12
+ * from `jwt.claims.user_id` when the caller hasn't set it explicitly.
13
+ */
14
+ const getConnections = async (cn = {}, seedAdapters) => {
15
+ const result = await (0, pgsql_test_1.getConnections)(cn, seedAdapters);
16
+ // Auto-derive principal_id from user_id on every setContext call
17
+ const origSetContext = result.db.setContext.bind(result.db);
18
+ result.db.setContext = (ctx) => {
19
+ if (ctx['jwt.claims.user_id'] && !('jwt.claims.principal_id' in ctx)) {
20
+ ctx = { ...ctx, 'jwt.claims.principal_id': ctx['jwt.claims.user_id'] };
21
+ }
22
+ origSetContext(ctx);
23
+ };
24
+ return result;
25
+ };
26
+ exports.getConnections = getConnections;
package/esm/connect.js ADDED
@@ -0,0 +1,22 @@
1
+ import { getConnections as getPgConnections } from 'pgsql-test';
2
+ /**
3
+ * Get connections with Constructive platform behavior.
4
+ *
5
+ * Delegates entirely to pgsql-test's getConnections (which already
6
+ * resolves pgpmDefaults + config file + env vars + overrides via
7
+ * getConnEnvOptions), then wraps the returned `db` client so that
8
+ * every `setContext()` call auto-derives `jwt.claims.principal_id`
9
+ * from `jwt.claims.user_id` when the caller hasn't set it explicitly.
10
+ */
11
+ export const getConnections = async (cn = {}, seedAdapters) => {
12
+ const result = await getPgConnections(cn, seedAdapters);
13
+ // Auto-derive principal_id from user_id on every setContext call
14
+ const origSetContext = result.db.setContext.bind(result.db);
15
+ result.db.setContext = (ctx) => {
16
+ if (ctx['jwt.claims.user_id'] && !('jwt.claims.principal_id' in ctx)) {
17
+ ctx = { ...ctx, 'jwt.claims.principal_id': ctx['jwt.claims.user_id'] };
18
+ }
19
+ origSetContext(ctx);
20
+ };
21
+ return result;
22
+ };
package/esm/index.js ADDED
@@ -0,0 +1,6 @@
1
+ // Re-export everything from pgsql-test
2
+ export * from 'pgsql-test';
3
+ // Export Constructive-specific getConnections with defaults baked in
4
+ export { getConnections } from './connect';
5
+ // Re-export snapshot utility
6
+ export { snapshot } from 'pgsql-test';
package/index.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ export * from 'pgsql-test';
2
+ export { getConnections } from './connect';
3
+ export type { GetConnectionOpts, GetConnectionResult } from './connect';
4
+ export { snapshot } from 'pgsql-test';
package/index.js ADDED
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.snapshot = exports.getConnections = void 0;
18
+ // Re-export everything from pgsql-test
19
+ __exportStar(require("pgsql-test"), exports);
20
+ // Export Constructive-specific getConnections with defaults baked in
21
+ var connect_1 = require("./connect");
22
+ Object.defineProperty(exports, "getConnections", { enumerable: true, get: function () { return connect_1.getConnections; } });
23
+ // Re-export snapshot utility
24
+ var pgsql_test_1 = require("pgsql-test");
25
+ Object.defineProperty(exports, "snapshot", { enumerable: true, get: function () { return pgsql_test_1.snapshot; } });
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "constructive-test",
3
+ "version": "0.2.0",
4
+ "author": "Constructive <developers@constructive.io>",
5
+ "description": "constructive-test extends pgsql-test with Constructive platform defaults: auto-derived principal_id, role mappings, and platform-specific test helpers",
6
+ "main": "index.js",
7
+ "module": "esm/index.js",
8
+ "types": "index.d.ts",
9
+ "homepage": "https://github.com/constructive-io/constructive",
10
+ "license": "MIT",
11
+ "publishConfig": {
12
+ "access": "public",
13
+ "directory": "dist"
14
+ },
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "https://github.com/constructive-io/constructive"
18
+ },
19
+ "bugs": {
20
+ "url": "https://github.com/constructive-io/constructive/issues"
21
+ },
22
+ "keywords": [
23
+ "constructive",
24
+ "postgres",
25
+ "postgresql",
26
+ "testing",
27
+ "integration-tests",
28
+ "database-testing",
29
+ "pg",
30
+ "rls",
31
+ "role-based-access",
32
+ "test-database",
33
+ "jest",
34
+ "principals",
35
+ "jwt",
36
+ "guc"
37
+ ],
38
+ "scripts": {
39
+ "clean": "makage clean",
40
+ "copy": "makage assets",
41
+ "prepack": "npm run build",
42
+ "build": "npm run clean; tsc; tsc -p tsconfig.esm.json; npm run copy",
43
+ "build:dev": "npm run clean; tsc --declarationMap; tsc -p tsconfig.esm.json; npm run copy",
44
+ "lint": "eslint . --fix",
45
+ "test": "jest --passWithNoTests",
46
+ "test:watch": "jest --watch"
47
+ },
48
+ "dependencies": {
49
+ "pgsql-test": "^4.16.5"
50
+ },
51
+ "devDependencies": {
52
+ "makage": "^0.3.0"
53
+ },
54
+ "gitHead": "03d9e7ed4808d1d957b495a33980022172494340"
55
+ }