@powersync/service-module-postgres-storage 0.0.0-dev-20250827072023 → 0.0.0-dev-20250828090417
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 +20 -14
- package/dist/.tsbuildinfo +1 -1
- package/dist/@types/storage/PostgresStorageProvider.d.ts +1 -1
- package/dist/@types/storage/PostgresSyncRulesStorage.d.ts +3 -1
- package/dist/@types/{utils/test-utils.d.ts → storage/PostgresTestStorageFactoryGenerator.d.ts} +3 -5
- package/dist/@types/storage/storage-index.d.ts +1 -0
- package/dist/@types/types/models/models-index.d.ts +0 -1
- package/dist/@types/utils/utils-index.d.ts +0 -1
- package/dist/migrations/scripts/1684951997326-init.js +0 -18
- package/dist/migrations/scripts/1684951997326-init.js.map +1 -1
- package/dist/storage/PostgresStorageProvider.js +1 -10
- package/dist/storage/PostgresStorageProvider.js.map +1 -1
- package/dist/storage/PostgresSyncRulesStorage.js +19 -6
- package/dist/storage/PostgresSyncRulesStorage.js.map +1 -1
- package/dist/{utils/test-utils.js → storage/PostgresTestStorageFactoryGenerator.js} +6 -22
- package/dist/storage/PostgresTestStorageFactoryGenerator.js.map +1 -0
- package/dist/storage/batch/PostgresBucketBatch.js +5 -2
- package/dist/storage/batch/PostgresBucketBatch.js.map +1 -1
- package/dist/storage/storage-index.js +1 -0
- package/dist/storage/storage-index.js.map +1 -1
- package/dist/types/models/models-index.js +0 -1
- package/dist/types/models/models-index.js.map +1 -1
- package/dist/utils/db.js +0 -1
- package/dist/utils/db.js.map +1 -1
- package/dist/utils/utils-index.js +0 -1
- package/dist/utils/utils-index.js.map +1 -1
- package/package.json +12 -11
- package/src/migrations/scripts/1684951997326-init.ts +0 -22
- package/src/storage/PostgresStorageProvider.ts +2 -13
- package/src/storage/PostgresSyncRulesStorage.ts +26 -7
- package/src/{utils/test-utils.ts → storage/PostgresTestStorageFactoryGenerator.ts} +5 -21
- package/src/storage/batch/PostgresBucketBatch.ts +4 -2
- package/src/storage/storage-index.ts +1 -0
- package/src/types/models/models-index.ts +0 -1
- package/src/utils/db.ts +0 -1
- package/src/utils/utils-index.ts +0 -1
- package/test/src/__snapshots__/storage_sync.test.ts.snap +110 -0
- package/test/src/util.ts +6 -3
- package/dist/@types/storage/PostgresReportStorageFactory.d.ts +0 -24
- package/dist/@types/types/models/SdkReporting.d.ts +0 -21
- package/dist/storage/PostgresReportStorageFactory.js +0 -238
- package/dist/storage/PostgresReportStorageFactory.js.map +0 -1
- package/dist/types/models/SdkReporting.js +0 -17
- package/dist/types/models/SdkReporting.js.map +0 -1
- package/dist/utils/test-utils.js.map +0 -1
- package/src/storage/PostgresReportStorageFactory.ts +0 -258
- package/src/types/models/SdkReporting.ts +0 -23
- package/test/src/__snapshots__/connection-report-storage.test.ts.snap +0 -215
- package/test/src/connection-report-storage.test.ts +0 -233
|
@@ -128,28 +128,6 @@ export const up: migrations.PowerSyncMigrationFunction = async (context) => {
|
|
|
128
128
|
CONSTRAINT unique_user_sync PRIMARY KEY (user_id, sync_rules_id)
|
|
129
129
|
);
|
|
130
130
|
`.execute();
|
|
131
|
-
await db.sql`
|
|
132
|
-
CREATE TABLE connection_report_events (
|
|
133
|
-
id TEXT PRIMARY KEY,
|
|
134
|
-
user_agent TEXT NOT NULL,
|
|
135
|
-
client_id TEXT NOT NULL,
|
|
136
|
-
user_id TEXT NOT NULL,
|
|
137
|
-
sdk TEXT NOT NULL,
|
|
138
|
-
jwt_exp TIMESTAMP WITH TIME ZONE,
|
|
139
|
-
connected_at TIMESTAMP WITH TIME ZONE NOT NULL,
|
|
140
|
-
disconnected_at TIMESTAMP WITH TIME ZONE
|
|
141
|
-
)
|
|
142
|
-
`.execute();
|
|
143
|
-
|
|
144
|
-
await db.sql`
|
|
145
|
-
CREATE INDEX sdk_list_index ON connection_report_events (connected_at, jwt_exp, disconnected_at)
|
|
146
|
-
`.execute();
|
|
147
|
-
|
|
148
|
-
await db.sql`CREATE INDEX sdk_user_id_index ON connection_report_events (user_id)`.execute();
|
|
149
|
-
|
|
150
|
-
await db.sql`CREATE INDEX sdk_client_id_index ON connection_report_events (client_id)`.execute();
|
|
151
|
-
|
|
152
|
-
await db.sql`CREATE INDEX sdk_index ON connection_report_events (sdk)`.execute();
|
|
153
131
|
});
|
|
154
132
|
};
|
|
155
133
|
|
|
@@ -5,9 +5,8 @@ import { storage } from '@powersync/service-core';
|
|
|
5
5
|
import { isPostgresStorageConfig, normalizePostgresStorageConfig, PostgresStorageConfig } from '../types/types.js';
|
|
6
6
|
import { dropTables } from '../utils/db.js';
|
|
7
7
|
import { PostgresBucketStorageFactory } from './PostgresBucketStorageFactory.js';
|
|
8
|
-
import { PostgresReportStorageFactory } from './PostgresReportStorageFactory.js';
|
|
9
8
|
|
|
10
|
-
export class PostgresStorageProvider implements storage.
|
|
9
|
+
export class PostgresStorageProvider implements storage.BucketStorageProvider {
|
|
11
10
|
get type() {
|
|
12
11
|
return lib_postgres.POSTGRES_CONNECTION_TYPE;
|
|
13
12
|
}
|
|
@@ -29,23 +28,13 @@ export class PostgresStorageProvider implements storage.StorageProvider {
|
|
|
29
28
|
config: normalizedConfig,
|
|
30
29
|
slot_name_prefix: options.resolvedConfig.slot_name_prefix
|
|
31
30
|
});
|
|
32
|
-
|
|
33
|
-
const reportStorageFactory = new PostgresReportStorageFactory({
|
|
34
|
-
config: normalizedConfig
|
|
35
|
-
});
|
|
36
|
-
|
|
37
31
|
return {
|
|
38
|
-
reportStorage: reportStorageFactory,
|
|
39
32
|
storage: storageFactory,
|
|
40
|
-
shutDown: async () =>
|
|
41
|
-
await storageFactory.db[Symbol.asyncDispose]();
|
|
42
|
-
await reportStorageFactory.db[Symbol.asyncDispose]();
|
|
43
|
-
},
|
|
33
|
+
shutDown: async () => storageFactory.db[Symbol.asyncDispose](),
|
|
44
34
|
tearDown: async () => {
|
|
45
35
|
logger.info(`Tearing down Postgres storage: ${normalizedConfig.database}...`);
|
|
46
36
|
await dropTables(storageFactory.db);
|
|
47
37
|
await storageFactory.db[Symbol.asyncDispose]();
|
|
48
|
-
await reportStorageFactory.db[Symbol.asyncDispose]();
|
|
49
38
|
return true;
|
|
50
39
|
}
|
|
51
40
|
} satisfies storage.ActiveStorage;
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import * as lib_postgres from '@powersync/lib-service-postgres';
|
|
2
2
|
import {
|
|
3
3
|
BroadcastIterable,
|
|
4
|
+
BucketChecksum,
|
|
4
5
|
CHECKPOINT_INVALIDATE_ALL,
|
|
5
6
|
CheckpointChanges,
|
|
7
|
+
CompactOptions,
|
|
6
8
|
GetCheckpointChangesOptions,
|
|
7
9
|
InternalOpId,
|
|
8
10
|
internalToExternalOpId,
|
|
9
11
|
LastValueSink,
|
|
10
12
|
maxLsn,
|
|
13
|
+
PartialChecksum,
|
|
11
14
|
ReplicationCheckpoint,
|
|
12
15
|
storage,
|
|
13
16
|
utils,
|
|
@@ -109,6 +112,10 @@ export class PostgresSyncRulesStorage
|
|
|
109
112
|
return new PostgresCompactor(this.db, this.group_id, options).compact();
|
|
110
113
|
}
|
|
111
114
|
|
|
115
|
+
async populatePersistentChecksumCache(options: Pick<CompactOptions, 'signal' | 'maxOpId'>): Promise<void> {
|
|
116
|
+
// no-op - checksum cache is not implemented for Postgres yet
|
|
117
|
+
}
|
|
118
|
+
|
|
112
119
|
lastWriteCheckpoint(filters: storage.SyncStorageLastWriteCheckpointFilters): Promise<bigint | null> {
|
|
113
120
|
return this.writeCheckpointAPI.lastWriteCheckpoint({
|
|
114
121
|
...filters,
|
|
@@ -571,6 +578,10 @@ export class PostgresSyncRulesStorage
|
|
|
571
578
|
return this.checksumCache.getChecksumMap(checkpoint, buckets);
|
|
572
579
|
}
|
|
573
580
|
|
|
581
|
+
clearChecksumCache() {
|
|
582
|
+
this.checksumCache.clear();
|
|
583
|
+
}
|
|
584
|
+
|
|
574
585
|
async terminate(options?: storage.TerminateOptions) {
|
|
575
586
|
if (!options || options?.clearStorage) {
|
|
576
587
|
await this.clear(options);
|
|
@@ -692,16 +703,24 @@ export class PostgresSyncRulesStorage
|
|
|
692
703
|
b.bucket_name;
|
|
693
704
|
`.rows<{ bucket: string; checksum_total: bigint; total: bigint; has_clear_op: number }>();
|
|
694
705
|
|
|
695
|
-
return new Map<string, storage.
|
|
706
|
+
return new Map<string, storage.PartialOrFullChecksum>(
|
|
696
707
|
results.map((doc) => {
|
|
708
|
+
const checksum = Number(BigInt(doc.checksum_total) & 0xffffffffn) & 0xffffffff;
|
|
709
|
+
|
|
697
710
|
return [
|
|
698
711
|
doc.bucket,
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
712
|
+
doc.has_clear_op == 1
|
|
713
|
+
? ({
|
|
714
|
+
// full checksum
|
|
715
|
+
bucket: doc.bucket,
|
|
716
|
+
count: Number(doc.total),
|
|
717
|
+
checksum
|
|
718
|
+
} satisfies BucketChecksum)
|
|
719
|
+
: ({
|
|
720
|
+
bucket: doc.bucket,
|
|
721
|
+
partialCount: Number(doc.total),
|
|
722
|
+
partialChecksum: checksum
|
|
723
|
+
} satisfies PartialChecksum)
|
|
705
724
|
];
|
|
706
725
|
})
|
|
707
726
|
);
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { framework, PowerSyncMigrationManager, ServiceContext, TestStorageOptions } from '@powersync/service-core';
|
|
2
2
|
import { PostgresMigrationAgent } from '../migrations/PostgresMigrationAgent.js';
|
|
3
3
|
import { normalizePostgresStorageConfig, PostgresStorageConfigDecoded } from '../types/types.js';
|
|
4
|
-
import {
|
|
5
|
-
import { PostgresBucketStorageFactory } from '../storage/PostgresBucketStorageFactory.js';
|
|
4
|
+
import { PostgresBucketStorageFactory } from './PostgresBucketStorageFactory.js';
|
|
6
5
|
|
|
7
6
|
export type PostgresTestStorageOptions = {
|
|
8
7
|
url: string;
|
|
@@ -13,7 +12,7 @@ export type PostgresTestStorageOptions = {
|
|
|
13
12
|
migrationAgent?: (config: PostgresStorageConfigDecoded) => PostgresMigrationAgent;
|
|
14
13
|
};
|
|
15
14
|
|
|
16
|
-
export
|
|
15
|
+
export const postgresTestSetup = (factoryOptions: PostgresTestStorageOptions) => {
|
|
17
16
|
const BASE_CONFIG = {
|
|
18
17
|
type: 'postgresql' as const,
|
|
19
18
|
uri: factoryOptions.url,
|
|
@@ -49,21 +48,6 @@ export function postgresTestSetup(factoryOptions: PostgresTestStorageOptions) {
|
|
|
49
48
|
};
|
|
50
49
|
|
|
51
50
|
return {
|
|
52
|
-
reportFactory: async (options?: TestStorageOptions) => {
|
|
53
|
-
try {
|
|
54
|
-
if (!options?.doNotClear) {
|
|
55
|
-
await migrate(framework.migrations.Direction.Up);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
return new PostgresReportStorageFactory({
|
|
59
|
-
config: TEST_CONNECTION_OPTIONS
|
|
60
|
-
});
|
|
61
|
-
} catch (ex) {
|
|
62
|
-
// Vitest does not display these errors nicely when using the `await using` syntx
|
|
63
|
-
console.error(ex, ex.cause);
|
|
64
|
-
throw ex;
|
|
65
|
-
}
|
|
66
|
-
},
|
|
67
51
|
factory: async (options?: TestStorageOptions) => {
|
|
68
52
|
try {
|
|
69
53
|
if (!options?.doNotClear) {
|
|
@@ -82,8 +66,8 @@ export function postgresTestSetup(factoryOptions: PostgresTestStorageOptions) {
|
|
|
82
66
|
},
|
|
83
67
|
migrate
|
|
84
68
|
};
|
|
85
|
-
}
|
|
69
|
+
};
|
|
86
70
|
|
|
87
|
-
export
|
|
71
|
+
export const PostgresTestStorageFactoryGenerator = (factoryOptions: PostgresTestStorageOptions) => {
|
|
88
72
|
return postgresTestSetup(factoryOptions).factory;
|
|
89
|
-
}
|
|
73
|
+
};
|
|
@@ -687,7 +687,8 @@ export class PostgresBucketBatch
|
|
|
687
687
|
// We store bytea colums for source keys
|
|
688
688
|
const beforeId = operation.beforeId;
|
|
689
689
|
const afterId = operation.afterId;
|
|
690
|
-
let
|
|
690
|
+
let sourceAfter = record.after;
|
|
691
|
+
let after = sourceAfter && this.sync_rules.applyRowContext(sourceAfter);
|
|
691
692
|
const sourceTable = record.sourceTable;
|
|
692
693
|
|
|
693
694
|
let existingBuckets: CurrentBucket[] = [];
|
|
@@ -825,7 +826,8 @@ export class PostgresBucketBatch
|
|
|
825
826
|
if (sourceTable.syncData) {
|
|
826
827
|
const { results: evaluated, errors: syncErrors } = this.sync_rules.evaluateRowWithErrors({
|
|
827
828
|
record: after,
|
|
828
|
-
sourceTable
|
|
829
|
+
sourceTable,
|
|
830
|
+
bucketIdTransformer: sync_rules.SqlSyncRules.versionedBucketIdTransformer(`${this.group_id}`)
|
|
829
831
|
});
|
|
830
832
|
|
|
831
833
|
for (const error of syncErrors) {
|
package/src/utils/db.ts
CHANGED
|
@@ -23,6 +23,5 @@ export const dropTables = async (client: lib_postgres.DatabaseClient) => {
|
|
|
23
23
|
await db.sql`DROP TABLE IF EXISTS custom_write_checkpoints`.execute();
|
|
24
24
|
await db.sql`DROP SEQUENCE IF EXISTS op_id_sequence`.execute();
|
|
25
25
|
await db.sql`DROP SEQUENCE IF EXISTS sync_rules_id_sequence`.execute();
|
|
26
|
-
await db.sql`DROP TABLE IF EXISTS connection_report_events`.execute();
|
|
27
26
|
});
|
|
28
27
|
};
|
package/src/utils/utils-index.ts
CHANGED
|
@@ -104,6 +104,116 @@ exports[`sync - postgres > compacting data - invalidate checkpoint 2`] = `
|
|
|
104
104
|
]
|
|
105
105
|
`;
|
|
106
106
|
|
|
107
|
+
exports[`sync - postgres > encodes sync rules id in buckes for streams 1`] = `
|
|
108
|
+
[
|
|
109
|
+
{
|
|
110
|
+
"checkpoint": {
|
|
111
|
+
"buckets": [
|
|
112
|
+
{
|
|
113
|
+
"bucket": "1#test|0[]",
|
|
114
|
+
"checksum": 920318466,
|
|
115
|
+
"count": 1,
|
|
116
|
+
"priority": 3,
|
|
117
|
+
"subscriptions": [
|
|
118
|
+
{
|
|
119
|
+
"default": 0,
|
|
120
|
+
},
|
|
121
|
+
],
|
|
122
|
+
},
|
|
123
|
+
],
|
|
124
|
+
"last_op_id": "1",
|
|
125
|
+
"streams": [
|
|
126
|
+
{
|
|
127
|
+
"errors": [],
|
|
128
|
+
"is_default": true,
|
|
129
|
+
"name": "test",
|
|
130
|
+
},
|
|
131
|
+
],
|
|
132
|
+
"write_checkpoint": undefined,
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
"data": {
|
|
137
|
+
"after": "0",
|
|
138
|
+
"bucket": "1#test|0[]",
|
|
139
|
+
"data": [
|
|
140
|
+
{
|
|
141
|
+
"checksum": 920318466,
|
|
142
|
+
"data": "{"id":"t1","description":"Test 1"}",
|
|
143
|
+
"object_id": "t1",
|
|
144
|
+
"object_type": "test",
|
|
145
|
+
"op": "PUT",
|
|
146
|
+
"op_id": "1",
|
|
147
|
+
"subkey": "02d285ac-4f96-5124-8fba-c6d1df992dd1",
|
|
148
|
+
},
|
|
149
|
+
],
|
|
150
|
+
"has_more": false,
|
|
151
|
+
"next_after": "1",
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
"checkpoint_complete": {
|
|
156
|
+
"last_op_id": "1",
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
]
|
|
160
|
+
`;
|
|
161
|
+
|
|
162
|
+
exports[`sync - postgres > encodes sync rules id in buckes for streams 2`] = `
|
|
163
|
+
[
|
|
164
|
+
{
|
|
165
|
+
"checkpoint": {
|
|
166
|
+
"buckets": [
|
|
167
|
+
{
|
|
168
|
+
"bucket": "2#test|0[]",
|
|
169
|
+
"checksum": 920318466,
|
|
170
|
+
"count": 1,
|
|
171
|
+
"priority": 3,
|
|
172
|
+
"subscriptions": [
|
|
173
|
+
{
|
|
174
|
+
"default": 0,
|
|
175
|
+
},
|
|
176
|
+
],
|
|
177
|
+
},
|
|
178
|
+
],
|
|
179
|
+
"last_op_id": "2",
|
|
180
|
+
"streams": [
|
|
181
|
+
{
|
|
182
|
+
"errors": [],
|
|
183
|
+
"is_default": true,
|
|
184
|
+
"name": "test",
|
|
185
|
+
},
|
|
186
|
+
],
|
|
187
|
+
"write_checkpoint": undefined,
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
"data": {
|
|
192
|
+
"after": "0",
|
|
193
|
+
"bucket": "2#test|0[]",
|
|
194
|
+
"data": [
|
|
195
|
+
{
|
|
196
|
+
"checksum": 920318466,
|
|
197
|
+
"data": "{"id":"t1","description":"Test 1"}",
|
|
198
|
+
"object_id": "t1",
|
|
199
|
+
"object_type": "test",
|
|
200
|
+
"op": "PUT",
|
|
201
|
+
"op_id": "2",
|
|
202
|
+
"subkey": "02d285ac-4f96-5124-8fba-c6d1df992dd1",
|
|
203
|
+
},
|
|
204
|
+
],
|
|
205
|
+
"has_more": false,
|
|
206
|
+
"next_after": "2",
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
"checkpoint_complete": {
|
|
211
|
+
"last_op_id": "2",
|
|
212
|
+
},
|
|
213
|
+
},
|
|
214
|
+
]
|
|
215
|
+
`;
|
|
216
|
+
|
|
107
217
|
exports[`sync - postgres > expired token 1`] = `
|
|
108
218
|
[
|
|
109
219
|
{
|
package/test/src/util.ts
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
2
|
import { fileURLToPath } from 'url';
|
|
3
|
-
import { normalizePostgresStorageConfig
|
|
3
|
+
import { normalizePostgresStorageConfig } from '../../src//types/types.js';
|
|
4
|
+
import { PostgresMigrationAgent } from '../../src/migrations/PostgresMigrationAgent.js';
|
|
5
|
+
import {
|
|
6
|
+
postgresTestSetup,
|
|
7
|
+
PostgresTestStorageFactoryGenerator
|
|
8
|
+
} from '../../src/storage/PostgresTestStorageFactoryGenerator.js';
|
|
4
9
|
import { env } from './env.js';
|
|
5
|
-
import { postgresTestSetup } from '../../src/utils/test-utils.js';
|
|
6
10
|
|
|
7
11
|
const __filename = fileURLToPath(import.meta.url);
|
|
8
12
|
const __dirname = path.dirname(__filename);
|
|
@@ -33,4 +37,3 @@ export const POSTGRES_STORAGE_SETUP = postgresTestSetup({
|
|
|
33
37
|
});
|
|
34
38
|
|
|
35
39
|
export const POSTGRES_STORAGE_FACTORY = POSTGRES_STORAGE_SETUP.factory;
|
|
36
|
-
export const POSTGRES_REPORT_STORAGE_FACTORY = POSTGRES_STORAGE_SETUP.reportFactory;
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import { storage } from '@powersync/service-core';
|
|
2
|
-
import * as pg_wire from '@powersync/service-jpgwire';
|
|
3
|
-
import { event_types } from '@powersync/service-types';
|
|
4
|
-
import * as lib_postgres from '@powersync/lib-service-postgres';
|
|
5
|
-
import { NormalizedPostgresStorageConfig } from '../types/types.js';
|
|
6
|
-
export type PostgresReportStorageOptions = {
|
|
7
|
-
config: NormalizedPostgresStorageConfig;
|
|
8
|
-
};
|
|
9
|
-
export declare class PostgresReportStorageFactory implements storage.ReportStorage {
|
|
10
|
-
protected options: PostgresReportStorageOptions;
|
|
11
|
-
readonly db: lib_postgres.DatabaseClient;
|
|
12
|
-
constructor(options: PostgresReportStorageOptions);
|
|
13
|
-
private parseJsDate;
|
|
14
|
-
private mapListCurrentConnectionsResponse;
|
|
15
|
-
private listConnectionsQuery;
|
|
16
|
-
private updateTableFilter;
|
|
17
|
-
reportClientConnection(data: event_types.ClientConnectionBucketData): Promise<void>;
|
|
18
|
-
reportClientDisconnection(data: event_types.ClientDisconnectionEventData): Promise<void>;
|
|
19
|
-
getConnectedClients(): Promise<event_types.ClientConnectionReportResponse>;
|
|
20
|
-
getClientConnectionReports(data: event_types.ClientConnectionReportRequest): Promise<event_types.ClientConnectionReportResponse>;
|
|
21
|
-
deleteOldConnectionData(data: event_types.DeleteOldConnectionData): Promise<void>;
|
|
22
|
-
[Symbol.asyncDispose](): Promise<void>;
|
|
23
|
-
prepareStatements(connection: pg_wire.PgConnection): Promise<void>;
|
|
24
|
-
}
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import * as t from 'ts-codec';
|
|
2
|
-
export declare const Sdks: t.ObjectCodec<{
|
|
3
|
-
sdk: t.IdentityCodec<t.CodecType.String>;
|
|
4
|
-
clients: t.IdentityCodec<t.CodecType.Number>;
|
|
5
|
-
users: t.IdentityCodec<t.CodecType.Number>;
|
|
6
|
-
}>;
|
|
7
|
-
export type Sdks = t.Encoded<typeof Sdks>;
|
|
8
|
-
export declare const SdkReporting: t.ObjectCodec<{
|
|
9
|
-
users: t.Codec<bigint, string | number, string, t.CodecProps>;
|
|
10
|
-
sdks: t.Union<t.Codec<{
|
|
11
|
-
data: {
|
|
12
|
-
sdk: string;
|
|
13
|
-
clients: number;
|
|
14
|
-
users: number;
|
|
15
|
-
}[];
|
|
16
|
-
} | undefined, {
|
|
17
|
-
data: string;
|
|
18
|
-
} | undefined, string, t.CodecProps>, t.Codec<null, null, t.CodecType.Null, t.CodecProps>>;
|
|
19
|
-
}>;
|
|
20
|
-
export type SdkReporting = t.Encoded<typeof SdkReporting>;
|
|
21
|
-
export type SdkReportingDecoded = t.Decoded<typeof SdkReporting>;
|
|
@@ -1,238 +0,0 @@
|
|
|
1
|
-
import { v4 } from 'uuid';
|
|
2
|
-
import * as lib_postgres from '@powersync/lib-service-postgres';
|
|
3
|
-
import { SdkReporting } from '../types/models/SdkReporting.js';
|
|
4
|
-
import { toInteger } from 'ix/util/tointeger.js';
|
|
5
|
-
import { logger } from '@powersync/lib-services-framework';
|
|
6
|
-
import { getStorageApplicationName } from '../utils/application-name.js';
|
|
7
|
-
import { STORAGE_SCHEMA_NAME } from '../utils/db.js';
|
|
8
|
-
export class PostgresReportStorageFactory {
|
|
9
|
-
options;
|
|
10
|
-
db;
|
|
11
|
-
constructor(options) {
|
|
12
|
-
this.options = options;
|
|
13
|
-
this.db = new lib_postgres.DatabaseClient({
|
|
14
|
-
config: options.config,
|
|
15
|
-
schema: STORAGE_SCHEMA_NAME,
|
|
16
|
-
applicationName: getStorageApplicationName()
|
|
17
|
-
});
|
|
18
|
-
this.db.registerListener({
|
|
19
|
-
connectionCreated: async (connection) => this.prepareStatements(connection)
|
|
20
|
-
});
|
|
21
|
-
}
|
|
22
|
-
parseJsDate(date) {
|
|
23
|
-
const year = date.getFullYear();
|
|
24
|
-
const month = date.getMonth();
|
|
25
|
-
const today = date.getDate();
|
|
26
|
-
const day = date.getDay();
|
|
27
|
-
return {
|
|
28
|
-
year,
|
|
29
|
-
month,
|
|
30
|
-
today,
|
|
31
|
-
day,
|
|
32
|
-
parsedDate: date
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
|
-
mapListCurrentConnectionsResponse(result) {
|
|
36
|
-
if (!result) {
|
|
37
|
-
return {
|
|
38
|
-
users: 0,
|
|
39
|
-
sdks: []
|
|
40
|
-
};
|
|
41
|
-
}
|
|
42
|
-
return {
|
|
43
|
-
users: Number(result.users),
|
|
44
|
-
sdks: result.sdks?.data || []
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
|
-
async listConnectionsQuery() {
|
|
48
|
-
return await this.db.sql `
|
|
49
|
-
WITH
|
|
50
|
-
filtered AS (
|
|
51
|
-
SELECT
|
|
52
|
-
*
|
|
53
|
-
FROM
|
|
54
|
-
connection_report_events
|
|
55
|
-
WHERE
|
|
56
|
-
disconnected_at IS NULL
|
|
57
|
-
AND jwt_exp > NOW() AT TIME ZONE 'UTC'
|
|
58
|
-
),
|
|
59
|
-
unique_users AS (
|
|
60
|
-
SELECT
|
|
61
|
-
COUNT(DISTINCT user_id) AS count
|
|
62
|
-
FROM
|
|
63
|
-
filtered
|
|
64
|
-
),
|
|
65
|
-
sdk_versions_array AS (
|
|
66
|
-
SELECT
|
|
67
|
-
sdk,
|
|
68
|
-
COUNT(DISTINCT client_id) AS clients,
|
|
69
|
-
COUNT(DISTINCT user_id) AS users
|
|
70
|
-
FROM
|
|
71
|
-
filtered
|
|
72
|
-
GROUP BY
|
|
73
|
-
sdk
|
|
74
|
-
)
|
|
75
|
-
SELECT
|
|
76
|
-
(
|
|
77
|
-
SELECT
|
|
78
|
-
COALESCE(count, 0)
|
|
79
|
-
FROM
|
|
80
|
-
unique_users
|
|
81
|
-
) AS users,
|
|
82
|
-
(
|
|
83
|
-
SELECT
|
|
84
|
-
JSON_AGG(ROW_TO_JSON(s))
|
|
85
|
-
FROM
|
|
86
|
-
sdk_versions_array s
|
|
87
|
-
) AS sdks;
|
|
88
|
-
`
|
|
89
|
-
.decoded(SdkReporting)
|
|
90
|
-
.first();
|
|
91
|
-
}
|
|
92
|
-
updateTableFilter() {
|
|
93
|
-
const { year, month, today } = this.parseJsDate(new Date());
|
|
94
|
-
const nextDay = today + 1;
|
|
95
|
-
return {
|
|
96
|
-
gte: new Date(year, month, today).toISOString(),
|
|
97
|
-
lt: new Date(year, month, nextDay).toISOString()
|
|
98
|
-
};
|
|
99
|
-
}
|
|
100
|
-
async reportClientConnection(data) {
|
|
101
|
-
const { sdk, connected_at, user_id, user_agent, jwt_exp, client_id } = data;
|
|
102
|
-
const connectIsoString = connected_at.toISOString();
|
|
103
|
-
const jwtExpIsoString = jwt_exp.toISOString();
|
|
104
|
-
const { gte, lt } = this.updateTableFilter();
|
|
105
|
-
const uuid = v4();
|
|
106
|
-
const result = await this.db.sql `
|
|
107
|
-
UPDATE connection_report_events
|
|
108
|
-
SET
|
|
109
|
-
connected_at = ${{ type: 1184, value: connectIsoString }},
|
|
110
|
-
sdk = ${{ type: 'varchar', value: sdk }},
|
|
111
|
-
user_agent = ${{ type: 'varchar', value: user_agent }},
|
|
112
|
-
jwt_exp = ${{ type: 1184, value: jwtExpIsoString }},
|
|
113
|
-
disconnected_at = NULL
|
|
114
|
-
WHERE
|
|
115
|
-
user_id = ${{ type: 'varchar', value: user_id }}
|
|
116
|
-
AND client_id = ${{ type: 'varchar', value: client_id }}
|
|
117
|
-
AND connected_at >= ${{ type: 1184, value: gte }}
|
|
118
|
-
AND connected_at < ${{ type: 1184, value: lt }};
|
|
119
|
-
`.execute();
|
|
120
|
-
if (result.results[1].status === 'UPDATE 0') {
|
|
121
|
-
await this.db.sql `
|
|
122
|
-
INSERT INTO
|
|
123
|
-
connection_report_events (
|
|
124
|
-
user_id,
|
|
125
|
-
client_id,
|
|
126
|
-
connected_at,
|
|
127
|
-
sdk,
|
|
128
|
-
user_agent,
|
|
129
|
-
jwt_exp,
|
|
130
|
-
id
|
|
131
|
-
)
|
|
132
|
-
VALUES
|
|
133
|
-
(
|
|
134
|
-
${{ type: 'varchar', value: user_id }},
|
|
135
|
-
${{ type: 'varchar', value: client_id }},
|
|
136
|
-
${{ type: 1184, value: connectIsoString }},
|
|
137
|
-
${{ type: 'varchar', value: sdk }},
|
|
138
|
-
${{ type: 'varchar', value: user_agent }},
|
|
139
|
-
${{ type: 1184, value: jwtExpIsoString }},
|
|
140
|
-
${{ type: 'varchar', value: uuid }}
|
|
141
|
-
)
|
|
142
|
-
`.execute();
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
async reportClientDisconnection(data) {
|
|
146
|
-
const { user_id, client_id, disconnected_at, connected_at } = data;
|
|
147
|
-
const disconnectIsoString = disconnected_at.toISOString();
|
|
148
|
-
const connectIsoString = connected_at.toISOString();
|
|
149
|
-
await this.db.sql `
|
|
150
|
-
UPDATE connection_report_events
|
|
151
|
-
SET
|
|
152
|
-
disconnected_at = ${{ type: 1184, value: disconnectIsoString }},
|
|
153
|
-
jwt_exp = NULL
|
|
154
|
-
WHERE
|
|
155
|
-
user_id = ${{ type: 'varchar', value: user_id }}
|
|
156
|
-
AND client_id = ${{ type: 'varchar', value: client_id }}
|
|
157
|
-
AND connected_at = ${{ type: 1184, value: connectIsoString }}
|
|
158
|
-
`.execute();
|
|
159
|
-
}
|
|
160
|
-
async getConnectedClients() {
|
|
161
|
-
const result = await this.listConnectionsQuery();
|
|
162
|
-
return this.mapListCurrentConnectionsResponse(result);
|
|
163
|
-
}
|
|
164
|
-
async getClientConnectionReports(data) {
|
|
165
|
-
const { start, end } = data;
|
|
166
|
-
const result = await this.db.sql `
|
|
167
|
-
WITH
|
|
168
|
-
filtered AS (
|
|
169
|
-
SELECT
|
|
170
|
-
*
|
|
171
|
-
FROM
|
|
172
|
-
connection_report_events
|
|
173
|
-
WHERE
|
|
174
|
-
connected_at >= ${{ type: 1184, value: start.toISOString() }}
|
|
175
|
-
AND connected_at <= ${{ type: 1184, value: end.toISOString() }}
|
|
176
|
-
),
|
|
177
|
-
unique_users AS (
|
|
178
|
-
SELECT
|
|
179
|
-
COUNT(DISTINCT user_id) AS count
|
|
180
|
-
FROM
|
|
181
|
-
filtered
|
|
182
|
-
),
|
|
183
|
-
sdk_versions_array AS (
|
|
184
|
-
SELECT
|
|
185
|
-
sdk,
|
|
186
|
-
COUNT(DISTINCT client_id) AS clients,
|
|
187
|
-
COUNT(DISTINCT user_id) AS users
|
|
188
|
-
FROM
|
|
189
|
-
filtered
|
|
190
|
-
GROUP BY
|
|
191
|
-
sdk
|
|
192
|
-
)
|
|
193
|
-
SELECT
|
|
194
|
-
(
|
|
195
|
-
SELECT
|
|
196
|
-
COALESCE(count, 0)
|
|
197
|
-
FROM
|
|
198
|
-
unique_users
|
|
199
|
-
) AS users,
|
|
200
|
-
(
|
|
201
|
-
SELECT
|
|
202
|
-
JSON_AGG(ROW_TO_JSON(s))
|
|
203
|
-
FROM
|
|
204
|
-
sdk_versions_array s
|
|
205
|
-
) AS sdks;
|
|
206
|
-
`
|
|
207
|
-
.decoded(SdkReporting)
|
|
208
|
-
.first();
|
|
209
|
-
return this.mapListCurrentConnectionsResponse(result);
|
|
210
|
-
}
|
|
211
|
-
async deleteOldConnectionData(data) {
|
|
212
|
-
const { date } = data;
|
|
213
|
-
const result = await this.db.sql `
|
|
214
|
-
DELETE FROM connection_report_events
|
|
215
|
-
WHERE
|
|
216
|
-
connected_at < ${{ type: 1184, value: date.toISOString() }}
|
|
217
|
-
AND (
|
|
218
|
-
disconnected_at IS NOT NULL
|
|
219
|
-
OR (
|
|
220
|
-
jwt_exp < NOW() AT TIME ZONE 'UTC'
|
|
221
|
-
AND disconnected_at IS NULL
|
|
222
|
-
)
|
|
223
|
-
);
|
|
224
|
-
`.execute();
|
|
225
|
-
const deletedRows = toInteger(result.results[1].status.split(' ')[1] || '0');
|
|
226
|
-
if (deletedRows > 0) {
|
|
227
|
-
logger.info(`TTL from ${date.toISOString()}: ${deletedRows} PostgresSQL rows have been removed from connection_report_events.`);
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
async [Symbol.asyncDispose]() {
|
|
231
|
-
await this.db[Symbol.asyncDispose]();
|
|
232
|
-
}
|
|
233
|
-
async prepareStatements(connection) {
|
|
234
|
-
// It should be possible to prepare statements for some common operations here.
|
|
235
|
-
// This has not been implemented yet.
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
//# sourceMappingURL=PostgresReportStorageFactory.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"PostgresReportStorageFactory.js","sourceRoot":"","sources":["../../src/storage/PostgresReportStorageFactory.ts"],"names":[],"mappings":"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"}
|