Skip to content

Commit dd41202

Browse files
authored
Merge pull request #1 from constructive-io/feat/default-roles-cleanup
chore: rely on default role bootstrap, drop inline timeouts, node 22
2 parents 6118905 + 01c2a7c commit dd41202

7 files changed

Lines changed: 161 additions & 114 deletions

File tree

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ jobs:
2929
- name: Setup Node.js
3030
uses: actions/setup-node@v4
3131
with:
32-
node-version: '20'
32+
node-version: '22'
3333
cache: 'pnpm'
3434

3535
- name: Install

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,10 +57,10 @@ import { getConnections, PgTestClient, seed } from 'pglite-test';
5757
let pg: PgTestClient, db: PgTestClient, teardown: () => Promise<void>;
5858

5959
beforeAll(async () => {
60+
// Standard app roles (authenticated, anonymous, …) are seeded for you, so
61+
// setContext({ role: 'authenticated' }) works with no manual CREATE ROLE.
6062
({ pg, db, teardown } = await getConnections(
61-
// PGlite boots as a single superuser, so roles used via setContext must be
62-
// created first (a future default-role bootstrap will remove this):
63-
{ pglite: { extensionSql: ['CREATE ROLE authenticated;'] } },
63+
{},
6464
[seed.pgpm(__dirname + '/..')] // deploy this package's pgpm module in-process
6565
));
6666
});

docs/pglite-vs-pgsql-test.md

Lines changed: 43 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -41,69 +41,74 @@ enabled. Set it in every package's scripts:
4141
}
4242
```
4343

44-
## 3. Generous timeouts for WASM cold-start (required on CI)
44+
## 3. Generous timeout for WASM cold-start (required on CI)
4545

4646
The first `getConnections()` compiles/loads the PGlite WASM module. On a cold CI
4747
runner this can exceed **Jest's default 5s hook timeout**, which makes
4848
`beforeAll` fail and `teardown` come back `undefined` (the failure we hit — the
49-
deploy logs actually land *after* the timeout). Two guards, applied everywhere:
50-
51-
```ts
52-
beforeAll(async () => {
53-
({ pg, db, teardown } = await getConnections(/* ... */));
54-
}, 120000); // explicit hook timeout
55-
```
49+
deploy logs actually land *after* the timeout). Fix it in **one place**
50+
`jest.config.js` — not with per-test inline timeouts:
5651

5752
```js
5853
// jest.config.js
5954
module.exports = {
6055
// ...
61-
testTimeout: 120000, // WASM cold-start room
56+
testTimeout: 120000, // WASM cold-start room (covers slow extension loads)
6257
};
6358
```
6459

6560
Loading a WASM **extension** (e.g. pgvector) is meaningfully slower than the
66-
bare instance, so vector suites especially need this. **This should be a default
67-
in any pglite-test boilerplate.**
61+
bare instance, so vector suites especially need this. The pglite boilerplate
62+
ships this in its generated `jest.config.js`, so test files carry no inline
63+
`beforeAll(..., 120000)` timeouts.
6864

69-
## 4. Roles are not auto-created (required, today)
65+
## 4. Roles are seeded by default (with an escape hatch)
7066

7167
On a real server `pgsql-test` bootstraps app roles (`anonymous` /
72-
`authenticated` / `administrator`) via `DbAdmin.createUserRole()` as part of
73-
`createdb`. PGlite has no `createdb` — the instance *is* the database — so that
74-
bootstrap never runs and **PGlite boots as a single superuser with no app
75-
roles**.
76-
77-
Any role used via `setContext({ role })` (and note `db`'s default context role
78-
is `anonymous`) must be created first, through `extensionSql`:
68+
`authenticated` / `administrator`) via
69+
`DbAdmin.createUserRole()` as part of `createdb`. PGlite has no `createdb` — the
70+
instance *is* the database — so `pglite-test` runs the equivalent bootstrap for
71+
you before seeding, using the same role generators (`generateCreateBaseRolesSQL`
72+
/ `generateCreateClientRoleSQL`) and the same attributes (`NOLOGIN`,
73+
`administrator` gets `BYPASSRLS`). So a bare `getConnections()` can switch into
74+
an app role with no manual `CREATE ROLE`:
7975

8076
```ts
81-
await getConnections(
82-
{ pglite: { extensionSql: ['CREATE ROLE authenticated;'] } },
83-
[seed.pgpm(__dirname + '/..')]
84-
);
77+
await getConnections({}, [seed.pgpm(__dirname + '/..')]);
78+
// db.setContext({ role: 'authenticated', ... }) just works
8579
```
8680

87-
The same `CREATE ROLE ... NOLOGIN` / `GRANT` statements our server bootstrap
88-
uses work verbatim in PGlite (it's real Postgres) — only the `LOGIN PASSWORD`
89-
second-connection bits are superfluous in-process.
81+
Custom role *names* come from `db.roles` (a `RoleMapping`), exactly like
82+
`pgsql-test`.
9083

91-
> **Boilerplate opportunity:** a default-role bootstrap in `pglite-test` (create
92-
> the group roles from `DEFAULT_ROLE_MAPPING`, `NOLOGIN`, idempotent) would make
93-
> it a true drop-in and remove this line. Until shipped, the boilerplate creates
94-
> the roles it uses explicitly.
84+
**Escape hatch:** to boot a lone superuser and manage your own roles/users, pass
85+
`pglite: { roles: false }` and create them in `extensionSql` (real Postgres DDL,
86+
verbatim from what you'd run on a server):
87+
88+
```ts
89+
await getConnections({
90+
pglite: {
91+
roles: false,
92+
extensionSql: ['CREATE ROLE app_writer NOLOGIN;', 'CREATE ROLE app_user LOGIN;']
93+
}
94+
});
95+
```
9596

9697
## 5. Extensions are provisioned out-of-band (required for extensions)
9798

9899
pgpm's `cleanSql` strips `CREATE EXTENSION` from migrations, and PGlite
99100
extensions are WASM modules that must be registered at construction. So an
100101
extension like pgvector needs three things wired together:
101102

102-
1. the module's migration keeps its `CREATE EXTENSION vector;` (deploy SQL) and
103-
the `.control` file lists it in `requires`;
103+
1. the `.control` file lists it in `requires` (how pgpm tracks the dependency);
104104
2. the WASM module is registered at construction: `pglite: { extensions: { vector } }`;
105105
3. it's installed at bootstrap: `pglite: { extensionSql: ['CREATE EXTENSION IF NOT EXISTS vector;'] }`.
106106

107+
Because `seed.pgpm()` deploys the module's **entire** plan on every suite, any
108+
suite that seeds a module containing a vector column must register the extension
109+
— even a suite that only tests RLS. Put steps 2–3 in one shared `connect()`
110+
helper the suites import, rather than repeating them per file.
111+
107112
```ts
108113
import { vector } from '@electric-sql/pglite-pgvector';
109114

@@ -154,7 +159,7 @@ steps:
154159
- uses: pnpm/action-setup@v4
155160
with: { version: 10 }
156161
- uses: actions/setup-node@v4
157-
with: { node-version: '20', cache: 'pnpm' }
162+
with: { node-version: '22', cache: 'pnpm' }
158163
- run: pnpm install --frozen-lockfile
159164
- run: cd ./packages/${{ matrix.package }} && pnpm test
160165
```
@@ -166,7 +171,8 @@ role bootstrap. This is the main reason a PGlite boilerplate is attractive.
166171

167172
The server/supabase suite maps roles in `pgpm.json` (`db.roles`, `useLocksForRoles`).
168173
The PGlite suite's `pgpm.json` is just the workspace manifest (`{"packages": ["packages/*"]}`);
169-
roles are handled per-suite via `extensionSql` (see §4).
174+
the standard roles are seeded automatically (see §4), and custom role *names*
175+
can still be passed per-suite via `db.roles`.
170176

171177
---
172178

@@ -185,8 +191,8 @@ roles are handled per-suite via `extensionSql` (see §4).
185191

186192
- [ ] deps: `pglite-test`, `@pgpmjs/pglite-adapter`, `@electric-sql/pglite` (+ `@electric-sql/pglite-pgvector` for a vector variant)
187193
- [ ] `test` scripts prefixed with `NODE_OPTIONS=--experimental-vm-modules`
188-
- [ ] `beforeAll(..., 120000)` + `testTimeout: 120000`
189-
- [ ] roles created via `pglite.extensionSql` (until default-role bootstrap ships)
190-
- [ ] extensions: `pglite.extensions` + `CREATE EXTENSION` in `extensionSql`, kept in migration + `.control`
194+
- [ ] single `testTimeout: 120000` in `jest.config.js` (no inline `beforeAll` timeouts)
195+
- [ ] standard app roles seeded by default (opt out with `pglite: { roles: false }`)
196+
- [ ] extensions: `pglite.extensions` + `CREATE EXTENSION` in `extensionSql`, declared in `.control`, shared via one `connect()` helper
191197
- [ ] services-free CI workflow
192198
- [ ] minimal `pgpm.json` (no `db.roles`)

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
"eslint-plugin-unused-imports": "^4.3.0",
3535
"jest": "^29.6.2",
3636
"lerna": "^8.2.3",
37-
"pglite-test": "^0.1.1",
37+
"pglite-test": "^0.2.1",
3838
"prettier": "^3.7.4",
3939
"rimraf": "4.4.1",
4040
"ts-jest": "^29.4.6",

packages/hello-world/__tests__/hello-world.test.ts

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,12 @@ const USER_2 = '550e8400-e29b-41d4-a716-446655440002';
99

1010
beforeAll(async () => {
1111
// No Postgres server, no `createdb`, no Docker: getConnections() spins up an
12-
// in-process PGlite instance and deploys the pgpm module in this package.
13-
//
14-
// PGlite boots as a single superuser, so any role used via setContext must be
15-
// created first. (A future default-role bootstrap in pglite-test will remove
16-
// this line; for now we create it explicitly, mirroring a server's role setup.)
17-
({ pg, db, teardown } = await getConnections(
18-
{ pglite: { extensionSql: ['CREATE ROLE authenticated;'] } },
19-
[seed.pgpm(__dirname + '/..')]
20-
));
21-
}, 120000);
12+
// in-process PGlite instance, deploys the pgpm module in this package, and
13+
// seeds the standard app roles — so setContext({ role: 'authenticated' })
14+
// works with no manual CREATE ROLE. (The cold-start timeout lives once in
15+
// jest.config.js, not inline here.)
16+
({ pg, db, teardown } = await getConnections({}, [seed.pgpm(__dirname + '/..')]));
17+
});
2218

2319
afterAll(async () => {
2420
await teardown();

packages/pgvector/__tests__/pgvector.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ beforeAll(async () => {
1818
},
1919
[seed.pgpm(__dirname + '/..')]
2020
));
21-
}, 120000);
21+
});
2222

2323
afterAll(async () => {
2424
await teardown();

0 commit comments

Comments
 (0)