@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.
- package/CHANGELOG.md +16 -8
- package/dist/replication/SnapshotQuery.d.ts +4 -1
- package/dist/replication/SnapshotQuery.js +3 -0
- package/dist/replication/SnapshotQuery.js.map +1 -1
- package/dist/replication/WalStream.js +34 -6
- package/dist/replication/WalStream.js.map +1 -1
- package/dist/replication/WalStreamReplicationJob.js +5 -1
- package/dist/replication/WalStreamReplicationJob.js.map +1 -1
- package/dist/replication/replication-utils.d.ts +4 -0
- package/dist/replication/replication-utils.js +46 -2
- package/dist/replication/replication-utils.js.map +1 -1
- package/package.json +10 -10
- package/src/replication/SnapshotQuery.ts +4 -1
- package/src/replication/WalStream.ts +34 -6
- package/src/replication/WalStreamReplicationJob.ts +5 -1
- package/src/replication/replication-utils.ts +60 -2
- package/test/src/checkpoints.test.ts +7 -4
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -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 {
|
|
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
|
+
};
|