@powersync/service-module-postgres 0.4.0 → 0.5.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.
Files changed (35) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/dist/api/PostgresRouteAPIAdapter.js +6 -1
  3. package/dist/api/PostgresRouteAPIAdapter.js.map +1 -1
  4. package/dist/module/PostgresModule.d.ts +4 -2
  5. package/dist/module/PostgresModule.js +11 -3
  6. package/dist/module/PostgresModule.js.map +1 -1
  7. package/dist/replication/ConnectionManagerFactory.d.ts +1 -1
  8. package/dist/replication/PgManager.js.map +1 -1
  9. package/dist/replication/PgRelation.js +2 -1
  10. package/dist/replication/PgRelation.js.map +1 -1
  11. package/dist/replication/WalStream.js +7 -6
  12. package/dist/replication/WalStream.js.map +1 -1
  13. package/dist/replication/WalStreamReplicator.d.ts +1 -0
  14. package/dist/replication/WalStreamReplicator.js +4 -0
  15. package/dist/replication/WalStreamReplicator.js.map +1 -1
  16. package/dist/replication/replication-utils.js +4 -4
  17. package/dist/replication/replication-utils.js.map +1 -1
  18. package/dist/types/types.d.ts +6 -2
  19. package/dist/utils/migration_lib.js +2 -1
  20. package/dist/utils/migration_lib.js.map +1 -1
  21. package/dist/utils/pgwire_utils.d.ts +6 -1
  22. package/dist/utils/pgwire_utils.js +20 -2
  23. package/dist/utils/pgwire_utils.js.map +1 -1
  24. package/package.json +10 -10
  25. package/src/api/PostgresRouteAPIAdapter.ts +6 -1
  26. package/src/module/PostgresModule.ts +22 -5
  27. package/src/replication/ConnectionManagerFactory.ts +1 -1
  28. package/src/replication/PgManager.ts +1 -0
  29. package/src/replication/PgRelation.ts +2 -1
  30. package/src/replication/WalStream.ts +13 -6
  31. package/src/replication/WalStreamReplicator.ts +5 -0
  32. package/src/replication/replication-utils.ts +9 -4
  33. package/src/utils/migration_lib.ts +2 -1
  34. package/src/utils/pgwire_utils.ts +21 -3
  35. package/tsconfig.tsbuildinfo +1 -1
package/package.json CHANGED
@@ -5,7 +5,7 @@
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
8
- "version": "0.4.0",
8
+ "version": "0.5.1",
9
9
  "main": "dist/index.js",
10
10
  "license": "FSL-1.1-Apache-2.0",
11
11
  "type": "module",
@@ -27,19 +27,19 @@
27
27
  "ts-codec": "^1.3.0",
28
28
  "uuid": "^9.0.1",
29
29
  "uri-js": "^4.4.1",
30
- "@powersync/lib-services-framework": "0.4.0",
31
- "@powersync/lib-service-postgres": "0.1.0",
32
- "@powersync/service-core": "0.15.0",
33
- "@powersync/service-jpgwire": "0.18.5",
30
+ "@powersync/lib-services-framework": "0.5.0",
31
+ "@powersync/lib-service-postgres": "0.2.0",
32
+ "@powersync/service-core": "0.16.1",
33
+ "@powersync/service-jpgwire": "0.19.0",
34
34
  "@powersync/service-jsonbig": "0.17.10",
35
- "@powersync/service-sync-rules": "0.23.1",
36
- "@powersync/service-types": "0.7.0"
35
+ "@powersync/service-sync-rules": "0.23.3",
36
+ "@powersync/service-types": "0.7.1"
37
37
  },
38
38
  "devDependencies": {
39
39
  "@types/uuid": "^9.0.4",
40
- "@powersync/service-core-tests": "0.3.0",
41
- "@powersync/service-module-mongodb-storage": "0.3.0",
42
- "@powersync/service-module-postgres-storage": "0.1.0"
40
+ "@powersync/service-core-tests": "0.3.2",
41
+ "@powersync/service-module-mongodb-storage": "0.3.2",
42
+ "@powersync/service-module-postgres-storage": "0.1.2"
43
43
  },
44
44
  "scripts": {
45
45
  "build": "tsc -b",
@@ -1,4 +1,5 @@
1
1
  import * as lib_postgres from '@powersync/lib-service-postgres';
2
+ import { ErrorCode, ServiceError } from '@powersync/lib-services-framework';
2
3
  import { api, ParseSyncRulesOptions } from '@powersync/service-core';
3
4
  import * as pgwire from '@powersync/service-jpgwire';
4
5
  import * as sync_rules from '@powersync/service-sync-rules';
@@ -228,7 +229,11 @@ FROM pg_replication_slots WHERE slot_name = $1 LIMIT 1;`,
228
229
  return Number(row.lsn_distance);
229
230
  }
230
231
 
231
- throw new Error(`Could not determine replication lag for slot ${slotName}`);
232
+ throw new ServiceError({
233
+ status: 500,
234
+ code: ErrorCode.PSYNC_S4001,
235
+ description: `Could not determine replication lag for slot ${slotName}`
236
+ });
232
237
  }
233
238
 
234
239
  async getReplicationHead(): Promise<string> {
@@ -1,4 +1,12 @@
1
- import { api, auth, ConfigurationFileSyncRulesProvider, modules, replication, system } from '@powersync/service-core';
1
+ import {
2
+ api,
3
+ auth,
4
+ ConfigurationFileSyncRulesProvider,
5
+ ConnectionTestResult,
6
+ modules,
7
+ replication,
8
+ system
9
+ } from '@powersync/service-core';
2
10
  import * as jpgwire from '@powersync/service-jpgwire';
3
11
  import { PostgresRouteAPIAdapter } from '../api/PostgresRouteAPIAdapter.js';
4
12
  import { SupabaseKeyCollector } from '../auth/SupabaseKeyCollector.js';
@@ -10,6 +18,7 @@ import { PUBLICATION_NAME } from '../replication/WalStream.js';
10
18
  import { WalStreamReplicator } from '../replication/WalStreamReplicator.js';
11
19
  import * as types from '../types/types.js';
12
20
  import { PostgresConnectionConfig } from '../types/types.js';
21
+ import { baseUri, NormalizedBasePostgresConnectionConfig } from '@powersync/lib-service-postgres';
13
22
 
14
23
  export class PostgresModule extends replication.ReplicationModule<types.PostgresConnectionConfig> {
15
24
  constructor() {
@@ -124,18 +133,26 @@ export class PostgresModule extends replication.ReplicationModule<types.Postgres
124
133
  });
125
134
  }
126
135
 
127
- async testConnection(config: PostgresConnectionConfig): Promise<void> {
136
+ async testConnection(config: PostgresConnectionConfig): Promise<ConnectionTestResult> {
128
137
  this.decodeConfig(config);
129
- const normalisedConfig = this.resolveConfig(this.decodedConfig!);
130
- const connectionManager = new PgManager(normalisedConfig, {
138
+ const normalizedConfig = this.resolveConfig(this.decodedConfig!);
139
+ return await this.testConnection(normalizedConfig);
140
+ }
141
+
142
+ static async testConnection(normalizedConfig: NormalizedBasePostgresConnectionConfig): Promise<ConnectionTestResult> {
143
+ // FIXME: This is not a complete implementation yet.
144
+ const connectionManager = new PgManager(normalizedConfig, {
131
145
  idleTimeout: 30_000,
132
146
  maxSize: 1
133
147
  });
134
148
  const connection = await connectionManager.snapshotConnection();
135
149
  try {
136
- return await checkSourceConfiguration(connection, PUBLICATION_NAME);
150
+ await checkSourceConfiguration(connection, PUBLICATION_NAME);
137
151
  } finally {
138
152
  await connectionManager.end();
139
153
  }
154
+ return {
155
+ connectionDescription: baseUri(normalizedConfig)
156
+ };
140
157
  }
141
158
  }
@@ -5,7 +5,7 @@ import { logger } from '@powersync/lib-services-framework';
5
5
 
6
6
  export class ConnectionManagerFactory {
7
7
  private readonly connectionManagers: PgManager[];
8
- private readonly dbConnectionConfig: NormalizedPostgresConnectionConfig;
8
+ public readonly dbConnectionConfig: NormalizedPostgresConnectionConfig;
9
9
 
10
10
  constructor(dbConnectionConfig: NormalizedPostgresConnectionConfig) {
11
11
  this.dbConnectionConfig = dbConnectionConfig;
@@ -50,6 +50,7 @@ export class PgManager {
50
50
  // for the full 6 minutes.
51
51
  // This we are constantly using the connection, we don't need any
52
52
  // custom keepalives.
53
+
53
54
  (connection as any)._socket.setTimeout(SNAPSHOT_SOCKET_TIMEOUT);
54
55
 
55
56
  // Disable statement timeout for snapshot queries.
@@ -1,3 +1,4 @@
1
+ import { ReplicationAssertionError, ServiceError } from '@powersync/lib-services-framework';
1
2
  import { storage } from '@powersync/service-core';
2
3
  import { PgoutputRelation } from '@powersync/service-jpgwire';
3
4
 
@@ -16,7 +17,7 @@ export function getRelId(source: PgoutputRelation): number {
16
17
  // Source types are wrong here
17
18
  const relId = (source as any).relationOid as number;
18
19
  if (!relId) {
19
- throw new Error(`No relation id!`);
20
+ throw new ReplicationAssertionError(`No relation id found`);
20
21
  }
21
22
  return relId;
22
23
  }
@@ -1,5 +1,11 @@
1
1
  import * as lib_postgres from '@powersync/lib-service-postgres';
2
- import { container, errors, logger } from '@powersync/lib-services-framework';
2
+ import {
3
+ container,
4
+ errors,
5
+ logger,
6
+ ReplicationAbortedError,
7
+ ReplicationAssertionError
8
+ } from '@powersync/lib-services-framework';
3
9
  import { getUuidReplicaIdentityBson, Metrics, SourceEntityDescriptor, storage } from '@powersync/service-core';
4
10
  import * as pgwire from '@powersync/service-jpgwire';
5
11
  import { DatabaseInputRow, SqliteRow, SqlSyncRules, TablePattern, toSyncRulesRow } from '@powersync/service-sync-rules';
@@ -133,7 +139,7 @@ export class WalStream {
133
139
  for (let row of tableRows) {
134
140
  const name = row.table_name as string;
135
141
  if (typeof row.relid != 'bigint') {
136
- throw new Error(`missing relid for ${name}`);
142
+ throw new ReplicationAssertionError(`Missing relid for ${name}`);
137
143
  }
138
144
  const relid = Number(row.relid as bigint);
139
145
 
@@ -294,7 +300,7 @@ export class WalStream {
294
300
  }
295
301
  }
296
302
 
297
- throw new Error('Unreachable');
303
+ throw new ReplicationAssertionError('Unreachable');
298
304
  }
299
305
 
300
306
  async estimatedCount(db: pgwire.PgConnection, table: storage.SourceTable): Promise<string> {
@@ -415,7 +421,7 @@ WHERE oid = $1::regclass`,
415
421
  lastLogIndex = at;
416
422
  }
417
423
  if (this.abort_signal.aborted) {
418
- throw new Error(`Aborted initial replication of ${this.slot_name}`);
424
+ throw new ReplicationAbortedError(`Aborted initial replication of ${this.slot_name}`);
419
425
  }
420
426
 
421
427
  for (const record of WalStream.getQueryData(rows)) {
@@ -441,7 +447,7 @@ WHERE oid = $1::regclass`,
441
447
 
442
448
  async handleRelation(batch: storage.BucketStorageBatch, descriptor: SourceEntityDescriptor, snapshot: boolean) {
443
449
  if (!descriptor.objectId && typeof descriptor.objectId != 'number') {
444
- throw new Error('objectId expected');
450
+ throw new ReplicationAssertionError(`objectId expected, got ${typeof descriptor.objectId}`);
445
451
  }
446
452
  const result = await this.storage.resolveTable({
447
453
  group_id: this.group_id,
@@ -484,6 +490,7 @@ WHERE oid = $1::regclass`,
484
490
  await db.query('COMMIT');
485
491
  } catch (e) {
486
492
  await db.query('ROLLBACK');
493
+ // TODO: Wrap with custom error type
487
494
  throw e;
488
495
  }
489
496
  } finally {
@@ -501,7 +508,7 @@ WHERE oid = $1::regclass`,
501
508
  if (table == null) {
502
509
  // We should always receive a replication message before the relation is used.
503
510
  // If we can't find it, it's a bug.
504
- throw new Error(`Missing relation cache for ${relationId}`);
511
+ throw new ReplicationAssertionError(`Missing relation cache for ${relationId}`);
505
512
  }
506
513
  return table;
507
514
  }
@@ -2,6 +2,7 @@ import { replication, storage } from '@powersync/service-core';
2
2
  import { ConnectionManagerFactory } from './ConnectionManagerFactory.js';
3
3
  import { cleanUpReplicationSlot } from './replication-utils.js';
4
4
  import { WalStreamReplicationJob } from './WalStreamReplicationJob.js';
5
+ import { PostgresModule } from '../module/PostgresModule.js';
5
6
 
6
7
  export interface WalStreamReplicatorOptions extends replication.AbstractReplicatorOptions {
7
8
  connectionFactory: ConnectionManagerFactory;
@@ -42,4 +43,8 @@ export class WalStreamReplicator extends replication.AbstractReplicator<WalStrea
42
43
  await super.stop();
43
44
  await this.connectionFactory.shutdown();
44
45
  }
46
+
47
+ async testConnection() {
48
+ return await PostgresModule.testConnection(this.connectionFactory.dbConnectionConfig);
49
+ }
45
50
  }
@@ -1,7 +1,7 @@
1
1
  import * as pgwire from '@powersync/service-jpgwire';
2
2
 
3
3
  import * as lib_postgres from '@powersync/lib-service-postgres';
4
- import { logger } from '@powersync/lib-services-framework';
4
+ import { ErrorCode, logger, ServiceError } from '@powersync/lib-services-framework';
5
5
  import { PatternResult, storage } from '@powersync/service-core';
6
6
  import * as sync_rules from '@powersync/service-sync-rules';
7
7
  import * as service_types from '@powersync/service-types';
@@ -117,17 +117,22 @@ $$ LANGUAGE plpgsql;`
117
117
  });
118
118
  const row = pgwire.pgwireRows(rs)[0];
119
119
  if (row == null) {
120
- throw new Error(
120
+ throw new ServiceError(
121
+ ErrorCode.PSYNC_S1141,
121
122
  `Publication '${publicationName}' does not exist. Run: \`CREATE PUBLICATION ${publicationName} FOR ALL TABLES\`, or read the documentation for details.`
122
123
  );
123
124
  }
124
125
  if (row.pubinsert == false || row.pubupdate == false || row.pubdelete == false || row.pubtruncate == false) {
125
- throw new Error(
126
+ throw new ServiceError(
127
+ ErrorCode.PSYNC_S1142,
126
128
  `Publication '${publicationName}' does not publish all changes. Create a publication using \`WITH (publish = "insert, update, delete, truncate")\` (the default).`
127
129
  );
128
130
  }
129
131
  if (row.pubviaroot) {
130
- throw new Error(`'${publicationName}' uses publish_via_partition_root, which is not supported.`);
132
+ throw new ServiceError(
133
+ ErrorCode.PSYNC_S1143,
134
+ `'${publicationName}' uses publish_via_partition_root, which is not supported.`
135
+ );
131
136
  }
132
137
  }
133
138
 
@@ -1,3 +1,4 @@
1
+ import { ServiceAssertionError } from '@powersync/lib-services-framework';
1
2
  import * as pgwire from '@powersync/service-jpgwire';
2
3
 
3
4
  export type MigrationFunction = (db: pgwire.PgConnection) => Promise<void>;
@@ -14,7 +15,7 @@ export class Migrations {
14
15
 
15
16
  add(id: number, name: string, up: MigrationFunction) {
16
17
  if (this.migrations.length > 0 && this.migrations[this.migrations.length - 1].id >= id) {
17
- throw new Error('Migration ids must be strictly incrementing');
18
+ throw new ServiceAssertionError('Migration ids must be strictly incrementing');
18
19
  }
19
20
  this.migrations.push({ id, up, name });
20
21
  }
@@ -1,7 +1,7 @@
1
1
  // Adapted from https://github.com/kagis/pgwire/blob/0dc927f9f8990a903f238737326e53ba1c8d094f/mod.js#L2218
2
2
 
3
3
  import * as pgwire from '@powersync/service-jpgwire';
4
- import { SqliteRow, toSyncRulesRow } from '@powersync/service-sync-rules';
4
+ import { DatabaseInputRow, SqliteRow, toSyncRulesRow } from '@powersync/service-sync-rules';
5
5
 
6
6
  /**
7
7
  * pgwire message -> SQLite row.
@@ -10,7 +10,7 @@ import { SqliteRow, toSyncRulesRow } from '@powersync/service-sync-rules';
10
10
  export function constructAfterRecord(message: pgwire.PgoutputInsert | pgwire.PgoutputUpdate): SqliteRow {
11
11
  const rawData = (message as any).afterRaw;
12
12
 
13
- const record = pgwire.decodeTuple(message.relation, rawData);
13
+ const record = decodeTuple(message.relation, rawData);
14
14
  return toSyncRulesRow(record);
15
15
  }
16
16
 
@@ -23,6 +23,24 @@ export function constructBeforeRecord(message: pgwire.PgoutputDelete | pgwire.Pg
23
23
  if (rawData == null) {
24
24
  return undefined;
25
25
  }
26
- const record = pgwire.decodeTuple(message.relation, rawData);
26
+ const record = decodeTuple(message.relation, rawData);
27
27
  return toSyncRulesRow(record);
28
28
  }
29
+
30
+ /**
31
+ * We need a high level of control over how values are decoded, to make sure there is no loss
32
+ * of precision in the process.
33
+ */
34
+ export function decodeTuple(relation: pgwire.PgoutputRelation, tupleRaw: Record<string, any>): DatabaseInputRow {
35
+ let result: Record<string, any> = {};
36
+ for (let columnName in tupleRaw) {
37
+ const rawval = tupleRaw[columnName];
38
+ const typeOid = (relation as any)._tupleDecoder._typeOids.get(columnName);
39
+ if (typeof rawval == 'string' && typeOid) {
40
+ result[columnName] = pgwire.PgType.decode(rawval, typeOid);
41
+ } else {
42
+ result[columnName] = rawval;
43
+ }
44
+ }
45
+ return result;
46
+ }