Skip to content

Commit 33d1c77

Browse files
committed
370: Support for connection parameters passed via the DB connection string
1 parent 90aac49 commit 33d1c77

File tree

12 files changed

+466
-25
lines changed

12 files changed

+466
-25
lines changed

libs/lib-mongodb/src/db/mongo.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -53,11 +53,11 @@ export function createMongoClient(config: BaseMongoConfigDecoded, options?: Mong
5353
password: normalized.password
5454
},
5555
// Time for connection to timeout
56-
connectTimeoutMS: MONGO_CONNECT_TIMEOUT_MS,
56+
connectTimeoutMS: normalized.connectTimeoutMS ?? MONGO_CONNECT_TIMEOUT_MS,
5757
// Time for individual requests to timeout
58-
socketTimeoutMS: MONGO_SOCKET_TIMEOUT_MS,
58+
socketTimeoutMS: normalized.socketTimeoutMS ?? MONGO_SOCKET_TIMEOUT_MS,
5959
// How long to wait for new primary selection
60-
serverSelectionTimeoutMS: 30_000,
60+
serverSelectionTimeoutMS: normalized.serverSelectionTimeoutMS ?? 30_000,
6161

6262
// Identify the client
6363
appName: options?.powersyncVersion ? `powersync-storage ${options.powersyncVersion}` : 'powersync-storage',
@@ -73,10 +73,10 @@ export function createMongoClient(config: BaseMongoConfigDecoded, options?: Mong
7373
// Avoid too many connections:
7474
// 1. It can overwhelm the source database.
7575
// 2. Processing too many queries in parallel can cause the process to run out of memory.
76-
maxPoolSize: options?.maxPoolSize ?? 8,
76+
maxPoolSize: normalized.maxPoolSize ?? options?.maxPoolSize ?? 8,
7777

7878
maxConnecting: 3,
79-
maxIdleTimeMS: 60_000
79+
maxIdleTimeMS: normalized.maxIdleTimeMS ?? 60_000
8080
});
8181
}
8282

libs/lib-mongodb/src/types/types.ts

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,13 @@ export const BaseMongoConfig = t.object({
1717
username: t.string.optional(),
1818
password: t.string.optional(),
1919

20-
reject_ip_ranges: t.array(t.string).optional()
20+
reject_ip_ranges: t.array(t.string).optional(),
21+
22+
connectTimeoutMS: t.number.optional(),
23+
socketTimeoutMS: t.number.optional(),
24+
serverSelectionTimeoutMS: t.number.optional(),
25+
maxPoolSize: t.number.optional(),
26+
maxIdleTimeMS: t.number.optional()
2127
});
2228

2329
export type BaseMongoConfig = t.Encoded<typeof BaseMongoConfig>;
@@ -29,6 +35,11 @@ export type NormalizedMongoConfig = {
2935
username: string;
3036
password: string;
3137
lookup: LookupFunction | undefined;
38+
connectTimeoutMS?: number;
39+
socketTimeoutMS?: number;
40+
serverSelectionTimeoutMS?: number;
41+
maxPoolSize?: number;
42+
maxIdleTimeMS?: number;
3243
};
3344

3445
/**
@@ -70,6 +81,19 @@ export function normalizeMongoConfig(options: BaseMongoConfigDecoded): Normalize
7081
throw new ServiceError(ErrorCode.PSYNC_S1105, `MongoDB connection: database required`);
7182
}
7283

84+
const parseQueryParam = (key: string): number | undefined => {
85+
const value = uri.searchParams.get(key);
86+
if (value == null) return undefined;
87+
const num = Number(value);
88+
if (isNaN(num) || num < 0) return undefined;
89+
return num;
90+
};
91+
const connectTimeoutMS = options.connectTimeoutMS ?? parseQueryParam('connectTimeoutMS');
92+
const socketTimeoutMS = options.socketTimeoutMS ?? parseQueryParam('socketTimeoutMS');
93+
const serverSelectionTimeoutMS = options.serverSelectionTimeoutMS ?? parseQueryParam('serverSelectionTimeoutMS');
94+
const maxPoolSize = options.maxPoolSize ?? parseQueryParam('maxPoolSize');
95+
const maxIdleTimeMS = options.maxIdleTimeMS ?? parseQueryParam('maxIdleTimeMS');
96+
7397
const lookupOptions: LookupOptions = {
7498
reject_ip_ranges: options.reject_ip_ranges ?? []
7599
};
@@ -82,6 +106,12 @@ export function normalizeMongoConfig(options: BaseMongoConfigDecoded): Normalize
82106
username,
83107
password,
84108

85-
lookup
109+
lookup,
110+
111+
connectTimeoutMS,
112+
socketTimeoutMS,
113+
serverSelectionTimeoutMS,
114+
maxPoolSize,
115+
maxIdleTimeMS
86116
};
87117
}

libs/lib-mongodb/test/src/config.test.ts

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { describe, expect, test } from 'vitest';
2-
import { normalizeMongoConfig } from '../../src/types/types.js';
3-
import { LookupAddress } from 'node:dns';
42
import { ErrorCode, ServiceError } from '@powersync/lib-services-framework';
53

4+
import { normalizeMongoConfig } from '../../src/types/types.js';
5+
66
describe('config', () => {
77
test('Should normalize a simple URI', () => {
88
const uri = 'mongodb://localhost:27017/powersync_test';
@@ -34,6 +34,48 @@ describe('config', () => {
3434
expect(normalized.database).equals('powersync_test');
3535
});
3636

37+
test('Should parse connection parameters from URI query string', () => {
38+
const normalized = normalizeMongoConfig({
39+
type: 'mongodb',
40+
uri: 'mongodb://user:pass@host/powersync_test?connectTimeoutMS=10000&socketTimeoutMS=60000&serverSelectionTimeoutMS=30000&maxPoolSize=10&maxIdleTimeMS=120000'
41+
});
42+
expect(normalized.connectTimeoutMS).equals(10000);
43+
expect(normalized.socketTimeoutMS).equals(60000);
44+
expect(normalized.serverSelectionTimeoutMS).equals(30000);
45+
expect(normalized.maxPoolSize).equals(10);
46+
expect(normalized.maxIdleTimeMS).equals(120000);
47+
});
48+
49+
test('Should prioritize explicit config over URI query params', () => {
50+
const normalized = normalizeMongoConfig({
51+
type: 'mongodb',
52+
uri: 'mongodb://user:pass@host/powersync_test?connectTimeoutMS=10000&maxPoolSize=10',
53+
connectTimeoutMS: 20000,
54+
maxPoolSize: 20
55+
});
56+
expect(normalized.connectTimeoutMS).equals(20000);
57+
expect(normalized.maxPoolSize).equals(20);
58+
});
59+
60+
test('Should handle partial query parameters', () => {
61+
const normalized = normalizeMongoConfig({
62+
type: 'mongodb',
63+
uri: 'mongodb://user:pass@host/powersync_test?connectTimeoutMS=10000'
64+
});
65+
expect(normalized.connectTimeoutMS).equals(10000);
66+
expect(normalized.socketTimeoutMS).toBeUndefined();
67+
expect(normalized.maxPoolSize).toBeUndefined();
68+
});
69+
70+
test('Should ignore invalid query parameter values', () => {
71+
const normalized = normalizeMongoConfig({
72+
type: 'mongodb',
73+
uri: 'mongodb://user:pass@host/powersync_test?connectTimeoutMS=invalid&maxPoolSize=-5'
74+
});
75+
expect(normalized.connectTimeoutMS).toBeUndefined();
76+
expect(normalized.maxPoolSize).toBeUndefined();
77+
});
78+
3779
test('Should normalize a replica set URI', () => {
3880
const uri =
3981
'mongodb://mongodb-0.mongodb.powersync.svc.cluster.local:27017,mongodb-1.mongodb.powersync.svc.cluster.local:27017,mongodb-2.mongodb.powersync.svc.cluster.local:27017/powersync_test?replicaSet=rs0';

libs/lib-postgres/src/types/types.ts

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,13 @@ export const BasePostgresConnectionConfig = t.object({
4646
*/
4747
slot_name_prefix: t.string.optional(),
4848

49-
max_pool_size: t.number.optional()
49+
max_pool_size: t.number.optional(),
50+
51+
connect_timeout: t.number.optional(),
52+
keepalives: t.number.optional(),
53+
keepalives_idle: t.number.optional(),
54+
keepalives_interval: t.number.optional(),
55+
keepalives_count: t.number.optional()
5056
});
5157

5258
export type BasePostgresConnectionConfig = t.Encoded<typeof BasePostgresConnectionConfig>;
@@ -83,6 +89,22 @@ export function normalizeConnectionConfig(options: BasePostgresConnectionConfigD
8389
const username = options.username ?? uri_username ?? '';
8490
const password = options.password ?? uri_password ?? '';
8591

92+
const queryString = uri.query ?? (options.uri && options.uri.includes('?') ? options.uri.split('?')[1].split('#')[0] : '');
93+
const queryParams = new URLSearchParams(queryString);
94+
const parseQueryParam = (key: string): number | undefined => {
95+
const value = queryParams.get(key);
96+
if (value == null) return undefined;
97+
const num = Number(value);
98+
if (isNaN(num) || num < 0) return undefined;
99+
return num;
100+
};
101+
102+
const connect_timeout = options.connect_timeout ?? parseQueryParam('connect_timeout');
103+
const keepalives = options.keepalives ?? parseQueryParam('keepalives');
104+
const keepalives_idle = options.keepalives_idle ?? parseQueryParam('keepalives_idle');
105+
const keepalives_interval = options.keepalives_interval ?? parseQueryParam('keepalives_interval');
106+
const keepalives_count = options.keepalives_count ?? parseQueryParam('keepalives_count');
107+
86108
const sslmode = options.sslmode ?? 'verify-full'; // Configuration not supported via URI
87109
const cacert = options.cacert;
88110

@@ -131,7 +153,13 @@ export function normalizeConnectionConfig(options: BasePostgresConnectionConfigD
131153
client_certificate: options.client_certificate ?? undefined,
132154
client_private_key: options.client_private_key ?? undefined,
133155

134-
max_pool_size: options.max_pool_size ?? 8
156+
max_pool_size: options.max_pool_size ?? 8,
157+
158+
connect_timeout,
159+
keepalives,
160+
keepalives_idle,
161+
keepalives_interval,
162+
keepalives_count
135163
} satisfies NormalizedBasePostgresConnectionConfig;
136164
}
137165

Lines changed: 136 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,147 @@
11
import { describe, expect, test } from 'vitest';
2+
23
import { normalizeConnectionConfig } from '../../src/types/types.js';
34

45
describe('config', () => {
5-
test('Should resolve database', () => {
6+
test('Should normalize a simple URI', () => {
67
const normalized = normalizeConnectionConfig({
78
type: 'postgresql',
89
uri: 'postgresql://postgres:postgres@localhost:4321/powersync_test'
910
});
1011
expect(normalized.database).equals('powersync_test');
12+
expect(normalized.hostname).equals('localhost');
13+
expect(normalized.port).equals(4321);
14+
expect(normalized.username).equals('postgres');
15+
expect(normalized.password).equals('postgres');
16+
});
17+
18+
test('Should normalize an URI with auth', () => {
19+
const uri = 'postgresql://user:pass@localhost:5432/powersync_test';
20+
const normalized = normalizeConnectionConfig({
21+
type: 'postgresql',
22+
uri
23+
});
24+
expect(normalized.database).equals('powersync_test');
25+
expect(normalized.username).equals('user');
26+
expect(normalized.password).equals('pass');
27+
});
28+
29+
test('Should normalize an URI with query params', () => {
30+
const normalized = normalizeConnectionConfig({
31+
type: 'postgresql',
32+
uri: 'postgresql://user:pass@host/db?other=param'
33+
});
34+
expect(normalized.database).equals('db');
35+
});
36+
37+
test('Should prioritize username and password that are specified explicitly', () => {
38+
const uri = 'postgresql://user:pass@localhost:5432/powersync_test';
39+
const normalized = normalizeConnectionConfig({
40+
type: 'postgresql',
41+
uri,
42+
username: 'user2',
43+
password: 'pass2'
44+
});
45+
expect(normalized.username).equals('user2');
46+
expect(normalized.password).equals('pass2');
47+
});
48+
49+
test('Should parse connection parameters from URI query string', () => {
50+
const normalized = normalizeConnectionConfig({
51+
type: 'postgresql',
52+
uri: 'postgresql://user:pass@host/db?connect_timeout=300&keepalives=1&keepalives_idle=60&keepalives_interval=10&keepalives_count=10'
53+
});
54+
expect(normalized.connect_timeout).equals(300);
55+
expect(normalized.keepalives).equals(1);
56+
expect(normalized.keepalives_idle).equals(60);
57+
expect(normalized.keepalives_interval).equals(10);
58+
expect(normalized.keepalives_count).equals(10);
59+
});
60+
61+
test('Should prioritize explicit config over URI query params', () => {
62+
const normalized = normalizeConnectionConfig({
63+
type: 'postgresql',
64+
uri: 'postgresql://user:pass@host/db?connect_timeout=300&keepalives_idle=60',
65+
connect_timeout: 600,
66+
keepalives_idle: 120
67+
});
68+
expect(normalized.connect_timeout).equals(600);
69+
expect(normalized.keepalives_idle).equals(120);
70+
});
71+
72+
test('Should handle partial query parameters', () => {
73+
const normalized = normalizeConnectionConfig({
74+
type: 'postgresql',
75+
uri: 'postgresql://user:pass@host/db?connect_timeout=300'
76+
});
77+
expect(normalized.connect_timeout).equals(300);
78+
expect(normalized.keepalives).toBeUndefined();
79+
expect(normalized.keepalives_idle).toBeUndefined();
80+
});
81+
82+
test('Should ignore invalid query parameter values', () => {
83+
const normalized = normalizeConnectionConfig({
84+
type: 'postgresql',
85+
uri: 'postgresql://user:pass@host/db?connect_timeout=invalid&keepalives_idle=-5'
86+
});
87+
expect(normalized.connect_timeout).toBeUndefined();
88+
expect(normalized.keepalives_idle).toBeUndefined();
89+
});
90+
91+
test('Should handle keepalives=0 to disable keepalives', () => {
92+
const normalized = normalizeConnectionConfig({
93+
type: 'postgresql',
94+
uri: 'postgresql://user:pass@host/db?keepalives=0'
95+
});
96+
expect(normalized.keepalives).equals(0);
97+
});
98+
99+
describe('errors', () => {
100+
test('Should throw error when no database specified', () => {
101+
['postgresql://user:pass@localhost:5432', 'postgresql://user:pass@localhost:5432/'].forEach((uri) => {
102+
expect(() =>
103+
normalizeConnectionConfig({
104+
type: 'postgresql',
105+
uri
106+
})
107+
).toThrow('[PSYNC_S1105] Postgres connection: database required');
108+
});
109+
});
110+
111+
test('Should throw error when URI has invalid scheme', () => {
112+
expect(() =>
113+
normalizeConnectionConfig({
114+
type: 'postgresql',
115+
uri: 'http://user:pass@localhost:5432/powersync_test'
116+
})
117+
).toThrow('[PSYNC_S1109] Invalid URI - protocol must be postgresql');
118+
});
119+
120+
test('Should throw error when hostname is missing', () => {
121+
expect(() =>
122+
normalizeConnectionConfig({
123+
type: 'postgresql',
124+
uri: 'postgresql://user:pass@/powersync_test'
125+
})
126+
).toThrow('[PSYNC_S1106] Postgres connection: hostname required');
127+
});
128+
129+
test('Should throw error when username is missing', () => {
130+
expect(() =>
131+
normalizeConnectionConfig({
132+
type: 'postgresql',
133+
uri: 'postgresql://localhost:5432/powersync_test'
134+
})
135+
).toThrow('[PSYNC_S1107] Postgres connection: username required');
136+
});
137+
138+
test('Should throw error when password is missing', () => {
139+
expect(() =>
140+
normalizeConnectionConfig({
141+
type: 'postgresql',
142+
uri: 'postgresql://user@localhost:5432/powersync_test'
143+
})
144+
).toThrow('[PSYNC_S1108] Postgres connection: password required');
145+
});
11146
});
12147
});

modules/module-mongodb/src/replication/MongoManager.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,11 @@ export class MongoManager extends BaseObserver<MongoManagerListener> {
2929

3030
lookup: options.lookup,
3131
// Time for connection to timeout
32-
connectTimeoutMS: 5_000,
32+
connectTimeoutMS: options.connectTimeoutMS ?? 5_000,
3333
// Time for individual requests to timeout
34-
socketTimeoutMS: 60_000,
34+
socketTimeoutMS: options.socketTimeoutMS ?? 60_000,
3535
// How long to wait for new primary selection
36-
serverSelectionTimeoutMS: 30_000,
36+
serverSelectionTimeoutMS: options.serverSelectionTimeoutMS ?? 30_000,
3737

3838
// Identify the client
3939
appName: `powersync ${POWERSYNC_VERSION}`,
@@ -47,10 +47,10 @@ export class MongoManager extends BaseObserver<MongoManagerListener> {
4747
// Avoid too many connections:
4848
// 1. It can overwhelm the source database.
4949
// 2. Processing too many queries in parallel can cause the process to run out of memory.
50-
maxPoolSize: 8,
50+
maxPoolSize: options.maxPoolSize ?? 8,
5151

5252
maxConnecting: 3,
53-
maxIdleTimeMS: 60_000,
53+
maxIdleTimeMS: options.maxIdleTimeMS ?? 60_000,
5454

5555
...BSON_DESERIALIZE_DATA_OPTIONS,
5656

modules/module-mongodb/src/types/types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,12 @@ export interface NormalizedMongoConnectionConfig {
5252
lookup?: LookupFunction;
5353

5454
postImages: PostImagesOption;
55+
56+
connectTimeoutMS?: number;
57+
socketTimeoutMS?: number;
58+
serverSelectionTimeoutMS?: number;
59+
maxPoolSize?: number;
60+
maxIdleTimeMS?: number;
5561
}
5662

5763
export const MongoConnectionConfig = lib_mongo.BaseMongoConfig.and(service_types.configFile.DataSourceConfig).and(

0 commit comments

Comments
 (0)