Skip to content

Commit 23cca04

Browse files
committed
feat: slonik major upgrade (#7897)
1 parent b83b0cc commit 23cca04

20 files changed

Lines changed: 808 additions & 474 deletions

File tree

integration-tests/testkit/seed.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -353,7 +353,7 @@ export function initSeed() {
353353
const result = await pool.any(psql`
354354
UPDATE "organization_access_tokens"
355355
SET "expires_at" = NOW()
356-
WHERE id IN (${psql.join(tokenIds, psql`, `)}) AND organization_id = ${organization.id}
356+
WHERE id IN (${psql.join(tokenIds, psql.fragment`, `)}) AND organization_id = ${organization.id}
357357
RETURNING
358358
"id"
359359
`);

integration-tests/tests/api/organization/members.spec.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { pollFor } from 'testkit/flow';
12
import { graphql } from 'testkit/gql';
23
import { ResourceAssignmentModeType } from 'testkit/gql/graphql';
34
import { execute } from 'testkit/graphql';
@@ -115,15 +116,19 @@ test.concurrent('cannot delete a role with members', async ({ expect }) => {
115116
test.concurrent('email invitation', async ({ expect }) => {
116117
const seed = initSeed();
117118
const { createOrg } = await seed.createOwner();
118-
const { inviteMember } = await createOrg();
119+
const { inviteMember, organization } = await createOrg();
119120

120121
const inviteEmail = seed.generateEmail();
121122
const invitationResult = await inviteMember(inviteEmail);
122123
const inviteCode = invitationResult.ok?.createdOrganizationInvitation.code;
123124
expect(inviteCode).toBeDefined();
124125

125-
const sentEmails = await history();
126-
expect(sentEmails).toContainEqual(expect.objectContaining({ to: inviteEmail }));
126+
await pollFor(async () => {
127+
const sentEmails = await history(inviteEmail);
128+
return sentEmails.length > 0;
129+
});
130+
const sentEmails = await history(inviteEmail);
131+
expect(sentEmails[0].subject).toEqual('You have been invited to join ' + organization.slug);
127132
});
128133

129134
test.concurrent('can not invite with role not existing in organization', async ({ expect }) => {

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,6 @@
175175
"eslint@8.57.1": "patches/eslint@8.57.1.patch",
176176
"@graphql-eslint/eslint-plugin@3.20.1": "patches/@graphql-eslint__eslint-plugin@3.20.1.patch",
177177
"got@14.4.7": "patches/got@14.4.7.patch",
178-
"slonik@30.4.4": "patches/slonik@30.4.4.patch",
179178
"@oclif/core@3.26.6": "patches/@oclif__core@3.26.6.patch",
180179
"oclif": "patches/oclif.patch",
181180
"graphiql": "patches/graphiql.patch",
@@ -184,7 +183,9 @@
184183
"p-cancelable@4.0.1": "patches/p-cancelable@4.0.1.patch",
185184
"bentocache": "patches/bentocache.patch",
186185
"@graphql-codegen/schema-ast": "patches/@graphql-codegen__schema-ast.patch",
187-
"@fastify/vite": "patches/@fastify__vite.patch"
186+
"@fastify/vite": "patches/@fastify__vite.patch",
187+
"@slonik/pg-driver": "patches/@slonik__pg-driver.patch",
188+
"slonik": "patches/slonik.patch"
188189
},
189190
"onlyBuiltDependencies": [
190191
"msw"

packages/internal/postgres/package.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,11 @@
77
".": "./src/index.ts"
88
},
99
"dependencies": {
10-
"slonik": "30.4.4",
11-
"slonik-interceptor-query-logging": "46.4.0"
10+
"@standard-schema/spec": "1.0.0",
11+
"slonik": "48.13.2",
12+
"slonik-interceptor-query-logging": "48.13.2",
13+
"slonik-sql-tag-raw": "48.13.2",
14+
"zod": "3.25.76"
1215
},
1316
"devDependencies": {
1417
"@hive/service-common": "workspace:*"

packages/internal/postgres/src/index.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,10 @@ export {
44
type CommonQueryMethods,
55
} from './postgres-database-pool';
66
export { type PostgresConnectionParamaters, createConnectionString } from './connection-string';
7-
export { psql } from './psql';
7+
export { psql, type TaggedTemplateLiteralInvocation } from './psql';
88
export {
99
UniqueIntegrityConstraintViolationError,
1010
ForeignKeyIntegrityConstraintViolationError,
11-
type TaggedTemplateLiteralInvocation,
1211
type PrimitiveValueExpression,
1312
type SerializableValue,
1413
type Interceptor,
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import type { PoolClient } from 'pg';
2+
import { sql, type DatabasePool, type DatabasePoolConnection } from 'slonik';
3+
import { raw } from 'slonik-sql-tag-raw';
4+
5+
/**
6+
* Bridge {slonik.DatabasePool} to an {pg.Pool} for usage with Postgraphile Workers.
7+
*
8+
* This is a very very pragmatic approach, since slonik moved away from using {pg.Pool} internally.
9+
* https://github.com/gajus/slonik/issues/768
10+
**/
11+
export class PgPoolBridge {
12+
constructor(private pool: DatabasePool) {}
13+
14+
end(): never {
15+
throw new Error('Not implemented.');
16+
}
17+
18+
async connect(): Promise<PoolClient> {
19+
const pgClientAvailableP = Promise.withResolvers<any>();
20+
const pgClientReleasedP = Promise.withResolvers<void>();
21+
22+
// slonik connect works in a way where the client is acquired for the callback handler closure only.
23+
// It is released once the Promise returned from the handler resolves.
24+
// We need to be a bit creative to support the "pg.Pool" API and obviously
25+
// trust graphile-workers to call the `release` method on our fake {pg.Client} :)
26+
// so the client is released back to the pool
27+
void this.pool.connect(async client => {
28+
pgClientAvailableP.resolve(new PgClientBridge(client, pgClientReleasedP.resolve));
29+
await pgClientReleasedP.promise;
30+
});
31+
32+
return pgClientAvailableP.promise;
33+
}
34+
35+
/** Some of graphile-workers logic just calls the `query` method on {pg.Pool} - without first acquiring a connection. */
36+
query(query: unknown, values?: unknown, callback?: unknown): any {
37+
// not used, but just in case so we can catch it in the future...
38+
if (typeof callback !== 'undefined') {
39+
throw new Error('PgClientBridge.query: callback not supported');
40+
}
41+
42+
if ((typeof query !== 'string' && typeof query !== 'object') || !query) {
43+
throw new Error('PgClientBridge.query: unsupported query input');
44+
}
45+
46+
if (typeof query === 'string') {
47+
return this.pool.query(sql.unsafe`${raw(query, values as any)}`);
48+
}
49+
50+
return this.pool.query(sql.unsafe`${raw((query as any).text as any, (query as any).values)}`);
51+
}
52+
53+
on(): this {
54+
// Note: we can skip setting up event handlers, as graphile workers is only setting up error handlers to avoid uncaught exceptions
55+
// For us, the error handlers are already set up by slonik
56+
// https://github.com/graphile/worker/blob/5650fbc4406fa3ce197b2ab582e08fd20974e50c/src/lib.ts#L351-L359
57+
return this;
58+
}
59+
60+
removeListener(): this {
61+
// Note: we can skip tearing down event handlers, since we ship setting them up in the first place
62+
// https://github.com/graphile/worker/blob/5650fbc4406fa3ce197b2ab582e08fd20974e50c/src/lib.ts#L351-L359
63+
return this;
64+
}
65+
}
66+
67+
class PgClientBridge {
68+
constructor(
69+
private connection: DatabasePoolConnection,
70+
/** This is invoked for again releasing the connection. */
71+
public release: () => void,
72+
) {}
73+
74+
query(query: unknown, values?: unknown, callback?: unknown): any {
75+
if (typeof callback !== 'undefined') {
76+
throw new Error('PgClientBridge.query: callback not supported');
77+
}
78+
79+
if ((typeof query !== 'string' && typeof query !== 'object') || !query) {
80+
throw new Error('PgClientBridge.query: unsupported query input');
81+
}
82+
83+
if (typeof query === 'string') {
84+
return this.connection.query(sql.unsafe`${raw(query, values as any)}`);
85+
}
86+
87+
return this.connection.query(
88+
sql.unsafe`${raw((query as any).text as any, (query as any).values)}`,
89+
);
90+
}
91+
}

0 commit comments

Comments
 (0)