@powersync/service-module-postgres 0.0.0-dev-20250611110033 → 0.0.0-dev-20250618131818

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.
@@ -1,4 +1,4 @@
1
- import { container, logger } from '@powersync/lib-services-framework';
1
+ import { container, logger, ReplicationAbortedError } from '@powersync/lib-services-framework';
2
2
  import { PgManager } from './PgManager.js';
3
3
  import { MissingReplicationSlotError, sendKeepAlive, WalStream } from './WalStream.js';
4
4
 
@@ -104,6 +104,10 @@ export class WalStreamReplicationJob extends replication.AbstractReplicationJob
104
104
  this.lastStream = stream;
105
105
  await stream.replicate();
106
106
  } catch (e) {
107
+ if (this.isStopped && e instanceof ReplicationAbortedError) {
108
+ // Ignore aborted errors
109
+ return;
110
+ }
107
111
  this.logger.error(`Replication error`, e);
108
112
  if (e.cause != null) {
109
113
  // Example:
@@ -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 { ErrorCode, logger, ServiceError } from '@powersync/lib-services-framework';
4
+ import { ErrorCode, logger, ServiceAssertionError, 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';
@@ -136,6 +136,61 @@ $$ LANGUAGE plpgsql;`
136
136
  }
137
137
  }
138
138
 
139
+ export async function checkTableRls(
140
+ db: pgwire.PgClient,
141
+ relationId: number
142
+ ): Promise<{ canRead: boolean; message?: string }> {
143
+ const rs = await lib_postgres.retriedQuery(db, {
144
+ statement: `
145
+ WITH user_info AS (
146
+ SELECT
147
+ current_user as username,
148
+ r.rolsuper,
149
+ r.rolbypassrls
150
+ FROM pg_roles r
151
+ WHERE r.rolname = current_user
152
+ )
153
+ SELECT
154
+ c.relname as tablename,
155
+ c.relrowsecurity as rls_enabled,
156
+ u.username as username,
157
+ u.rolsuper as is_superuser,
158
+ u.rolbypassrls as bypasses_rls
159
+ FROM pg_class c
160
+ CROSS JOIN user_info u
161
+ WHERE c.oid = $1::oid;
162
+ `,
163
+ params: [{ type: 'int4', value: relationId }]
164
+ });
165
+
166
+ const rows = pgwire.pgwireRows<{
167
+ rls_enabled: boolean;
168
+ tablename: string;
169
+ username: string;
170
+ is_superuser: boolean;
171
+ bypasses_rls: boolean;
172
+ }>(rs);
173
+ if (rows.length == 0) {
174
+ // Not expected, since we already got the oid
175
+ throw new ServiceAssertionError(`Table with OID ${relationId} does not exist.`);
176
+ }
177
+ const row = rows[0];
178
+ if (row.is_superuser || row.bypasses_rls) {
179
+ // Bypasses RLS automatically.
180
+ return { canRead: true };
181
+ }
182
+
183
+ if (row.rls_enabled) {
184
+ // Don't skip, since we _may_ still be able to get results.
185
+ return {
186
+ canRead: false,
187
+ message: `[${ErrorCode.PSYNC_S1145}] Row Level Security is enabled on table "${row.tablename}". To make sure that ${row.username} can read the table, run: 'ALTER ROLE ${row.username} BYPASSRLS'.`
188
+ };
189
+ }
190
+
191
+ return { canRead: true };
192
+ }
193
+
139
194
  export interface GetDebugTablesInfoOptions {
140
195
  db: pgwire.PgClient;
141
196
  publicationName: string;
@@ -309,6 +364,9 @@ export async function getDebugTableInfo(options: GetDebugTableInfoOptions): Prom
309
364
  };
310
365
  }
311
366
 
367
+ const rlsCheck = await checkTableRls(db, relationId);
368
+ const rlsError = rlsCheck.canRead ? null : { message: rlsCheck.message!, level: 'warning' };
369
+
312
370
  return {
313
371
  schema: schema,
314
372
  name: name,
@@ -316,7 +374,7 @@ export async function getDebugTableInfo(options: GetDebugTableInfoOptions): Prom
316
374
  replication_id: id_columns.map((c) => c.name),
317
375
  data_queries: syncData,
318
376
  parameter_queries: syncParameters,
319
- errors: [id_columns_error, selectError, replicateError].filter(
377
+ errors: [id_columns_error, selectError, replicateError, rlsError].filter(
320
378
  (error) => error != null
321
379
  ) as service_types.ReplicationError[]
322
380
  };
@@ -1,7 +1,7 @@
1
1
  import { PostgresRouteAPIAdapter } from '@module/api/PostgresRouteAPIAdapter.js';
2
- import { checkpointUserId, createWriteCheckpoint } from '@powersync/service-core';
2
+ import { checkpointUserId, createWriteCheckpoint, TestStorageFactory } from '@powersync/service-core';
3
3
  import { describe, test } from 'vitest';
4
- import { INITIALIZED_MONGO_STORAGE_FACTORY } from './util.js';
4
+ import { describeWithStorage } from './util.js';
5
5
  import { WalStreamTestContext } from './wal_stream_utils.js';
6
6
 
7
7
  import timers from 'node:timers/promises';
@@ -12,8 +12,11 @@ const BASIC_SYNC_RULES = `bucket_definitions:
12
12
  - SELECT id, description, other FROM "test_data"`;
13
13
 
14
14
  describe('checkpoint tests', () => {
15
+ describeWithStorage({}, checkpointTests);
16
+ });
17
+
18
+ const checkpointTests = (factory: TestStorageFactory) => {
15
19
  test('write checkpoints', { timeout: 50_000 }, async () => {
16
- const factory = INITIALIZED_MONGO_STORAGE_FACTORY;
17
20
  await using context = await WalStreamTestContext.open(factory);
18
21
 
19
22
  await context.updateSyncRules(BASIC_SYNC_RULES);
@@ -78,4 +81,4 @@ describe('checkpoint tests', () => {
78
81
  controller.abort();
79
82
  }
80
83
  });
81
- });
84
+ };