@powersync/service-module-mongodb-storage 0.0.0-dev-20250829094737 → 0.0.0-dev-20250903064005
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 +31 -7
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/migrations/db/migrations/1741697235857-bucket-state-index.js +1 -4
- package/dist/migrations/db/migrations/1741697235857-bucket-state-index.js.map +1 -1
- package/dist/migrations/db/migrations/1752661449910-connection-reporting.d.ts +3 -0
- package/dist/migrations/db/migrations/1752661449910-connection-reporting.js +36 -0
- package/dist/migrations/db/migrations/1752661449910-connection-reporting.js.map +1 -0
- package/dist/storage/MongoBucketStorage.d.ts +3 -2
- package/dist/storage/MongoBucketStorage.js +5 -3
- package/dist/storage/MongoBucketStorage.js.map +1 -1
- package/dist/storage/MongoReportStorage.d.ts +17 -0
- package/dist/storage/MongoReportStorage.js +152 -0
- package/dist/storage/MongoReportStorage.js.map +1 -0
- package/dist/storage/implementation/MongoBucketBatch.js +1 -1
- package/dist/storage/implementation/MongoBucketBatch.js.map +1 -1
- package/dist/storage/implementation/MongoChecksums.d.ts +45 -13
- package/dist/storage/implementation/MongoChecksums.js +148 -135
- package/dist/storage/implementation/MongoChecksums.js.map +1 -1
- package/dist/storage/implementation/MongoCompactor.js +23 -21
- package/dist/storage/implementation/MongoCompactor.js.map +1 -1
- package/dist/storage/implementation/MongoStorageProvider.d.ts +1 -1
- package/dist/storage/implementation/MongoStorageProvider.js +7 -3
- package/dist/storage/implementation/MongoStorageProvider.js.map +1 -1
- package/dist/storage/implementation/MongoSyncBucketStorage.d.ts +5 -2
- package/dist/storage/implementation/MongoSyncBucketStorage.js +4 -4
- package/dist/storage/implementation/MongoSyncBucketStorage.js.map +1 -1
- package/dist/storage/implementation/PersistedBatch.js +1 -1
- package/dist/storage/implementation/PersistedBatch.js.map +1 -1
- package/dist/storage/implementation/db.d.ts +10 -1
- package/dist/storage/implementation/db.js +25 -0
- package/dist/storage/implementation/db.js.map +1 -1
- package/dist/storage/implementation/models.d.ts +7 -0
- package/dist/storage/storage-index.d.ts +3 -2
- package/dist/storage/storage-index.js +3 -2
- package/dist/storage/storage-index.js.map +1 -1
- package/dist/utils/test-utils.d.ts +13 -0
- package/dist/utils/test-utils.js +40 -0
- package/dist/utils/test-utils.js.map +1 -0
- package/dist/{storage/implementation → utils}/util.d.ts +1 -6
- package/dist/{storage/implementation → utils}/util.js +0 -15
- package/dist/utils/util.js.map +1 -0
- package/dist/utils/utils-index.d.ts +2 -0
- package/dist/utils/utils-index.js +3 -0
- package/dist/utils/utils-index.js.map +1 -0
- package/package.json +7 -7
- package/src/index.ts +1 -0
- package/src/migrations/db/migrations/1741697235857-bucket-state-index.ts +1 -7
- package/src/migrations/db/migrations/1752661449910-connection-reporting.ts +58 -0
- package/src/storage/MongoBucketStorage.ts +5 -4
- package/src/storage/MongoReportStorage.ts +174 -0
- package/src/storage/implementation/MongoBucketBatch.ts +1 -1
- package/src/storage/implementation/MongoChecksums.ts +172 -150
- package/src/storage/implementation/MongoCompactor.ts +23 -22
- package/src/storage/implementation/MongoStorageProvider.ts +9 -4
- package/src/storage/implementation/MongoSyncBucketStorage.ts +11 -5
- package/src/storage/implementation/PersistedBatch.ts +1 -1
- package/src/storage/implementation/db.ts +31 -0
- package/src/storage/implementation/models.ts +7 -0
- package/src/storage/storage-index.ts +3 -2
- package/src/utils/test-utils.ts +57 -0
- package/src/{storage/implementation → utils}/util.ts +2 -18
- package/src/utils/utils-index.ts +2 -0
- package/test/src/__snapshots__/connection-report-storage.test.ts.snap +215 -0
- package/test/src/__snapshots__/storage.test.ts.snap +17 -1
- package/test/src/connection-report-storage.test.ts +133 -0
- package/test/src/storage.test.ts +38 -1
- package/test/src/storage_compacting.test.ts +120 -5
- package/test/src/util.ts +6 -2
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/storage/implementation/MongoTestStorageFactoryGenerator.d.ts +0 -7
- package/dist/storage/implementation/MongoTestStorageFactoryGenerator.js +0 -18
- package/dist/storage/implementation/MongoTestStorageFactoryGenerator.js.map +0 -1
- package/dist/storage/implementation/util.js.map +0 -1
- package/src/storage/implementation/MongoTestStorageFactoryGenerator.ts +0 -28
|
@@ -4,8 +4,9 @@ import { POWERSYNC_VERSION, storage } from '@powersync/service-core';
|
|
|
4
4
|
import { MongoStorageConfig } from '../../types/types.js';
|
|
5
5
|
import { MongoBucketStorage } from '../MongoBucketStorage.js';
|
|
6
6
|
import { PowerSyncMongo } from './db.js';
|
|
7
|
+
import { MongoReportStorage } from '../MongoReportStorage.js';
|
|
7
8
|
|
|
8
|
-
export class MongoStorageProvider implements storage.
|
|
9
|
+
export class MongoStorageProvider implements storage.StorageProvider {
|
|
9
10
|
get type() {
|
|
10
11
|
return lib_mongo.MONGO_CONNECTION_TYPE;
|
|
11
12
|
}
|
|
@@ -37,15 +38,19 @@ export class MongoStorageProvider implements storage.BucketStorageProvider {
|
|
|
37
38
|
await client.connect();
|
|
38
39
|
|
|
39
40
|
const database = new PowerSyncMongo(client, { database: resolvedConfig.storage.database });
|
|
40
|
-
const
|
|
41
|
+
const syncStorageFactory = new MongoBucketStorage(database, {
|
|
41
42
|
// TODO currently need the entire resolved config due to this
|
|
42
43
|
slot_name_prefix: resolvedConfig.slot_name_prefix
|
|
43
44
|
});
|
|
45
|
+
|
|
46
|
+
// Storage factory for reports
|
|
47
|
+
const reportStorageFactory = new MongoReportStorage(database);
|
|
44
48
|
return {
|
|
45
|
-
storage:
|
|
49
|
+
storage: syncStorageFactory,
|
|
50
|
+
reportStorage: reportStorageFactory,
|
|
46
51
|
shutDown: async () => {
|
|
47
52
|
shuttingDown = true;
|
|
48
|
-
await
|
|
53
|
+
await syncStorageFactory[Symbol.asyncDispose]();
|
|
49
54
|
await client.close();
|
|
50
55
|
},
|
|
51
56
|
tearDown: () => {
|
|
@@ -31,11 +31,16 @@ import { MongoBucketStorage } from '../MongoBucketStorage.js';
|
|
|
31
31
|
import { PowerSyncMongo } from './db.js';
|
|
32
32
|
import { BucketDataDocument, BucketDataKey, BucketStateDocument, SourceKey, SourceTableDocument } from './models.js';
|
|
33
33
|
import { MongoBucketBatch } from './MongoBucketBatch.js';
|
|
34
|
-
import { MongoChecksums } from './MongoChecksums.js';
|
|
34
|
+
import { MongoChecksumOptions, MongoChecksums } from './MongoChecksums.js';
|
|
35
35
|
import { MongoCompactor } from './MongoCompactor.js';
|
|
36
36
|
import { MongoParameterCompactor } from './MongoParameterCompactor.js';
|
|
37
37
|
import { MongoWriteCheckpointAPI } from './MongoWriteCheckpointAPI.js';
|
|
38
|
-
import { idPrefixFilter, mapOpEntry, readSingleBatch, setSessionSnapshotTime } from '
|
|
38
|
+
import { idPrefixFilter, mapOpEntry, readSingleBatch, setSessionSnapshotTime } from '../../utils/util.js';
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
export interface MongoSyncBucketStorageOptions {
|
|
42
|
+
checksumOptions?: MongoChecksumOptions;
|
|
43
|
+
}
|
|
39
44
|
|
|
40
45
|
export class MongoSyncBucketStorage
|
|
41
46
|
extends BaseObserver<storage.SyncRulesBucketStorageListener>
|
|
@@ -52,14 +57,15 @@ export class MongoSyncBucketStorage
|
|
|
52
57
|
public readonly group_id: number,
|
|
53
58
|
private readonly sync_rules: storage.PersistedSyncRulesContent,
|
|
54
59
|
public readonly slot_name: string,
|
|
55
|
-
writeCheckpointMode
|
|
60
|
+
writeCheckpointMode?: storage.WriteCheckpointMode,
|
|
61
|
+
options?: MongoSyncBucketStorageOptions
|
|
56
62
|
) {
|
|
57
63
|
super();
|
|
58
64
|
this.db = factory.db;
|
|
59
|
-
this.checksums = new MongoChecksums(this.db, this.group_id);
|
|
65
|
+
this.checksums = new MongoChecksums(this.db, this.group_id, options?.checksumOptions);
|
|
60
66
|
this.writeCheckpointAPI = new MongoWriteCheckpointAPI({
|
|
61
67
|
db: this.db,
|
|
62
|
-
mode: writeCheckpointMode,
|
|
68
|
+
mode: writeCheckpointMode ?? storage.WriteCheckpointMode.MANAGED,
|
|
63
69
|
sync_rules_id: group_id
|
|
64
70
|
});
|
|
65
71
|
}
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
BucketParameterDocument,
|
|
9
9
|
BucketStateDocument,
|
|
10
10
|
CheckpointEventDocument,
|
|
11
|
+
ClientConnectionDocument,
|
|
11
12
|
CurrentDataDocument,
|
|
12
13
|
CustomWriteCheckpointDocument,
|
|
13
14
|
IdSequenceDocument,
|
|
@@ -37,6 +38,7 @@ export class PowerSyncMongo {
|
|
|
37
38
|
readonly locks: mongo.Collection<lib_mongo.locks.Lock>;
|
|
38
39
|
readonly bucket_state: mongo.Collection<BucketStateDocument>;
|
|
39
40
|
readonly checkpoint_events: mongo.Collection<CheckpointEventDocument>;
|
|
41
|
+
readonly connection_report_events: mongo.Collection<ClientConnectionDocument>;
|
|
40
42
|
|
|
41
43
|
readonly client: mongo.MongoClient;
|
|
42
44
|
readonly db: mongo.Db;
|
|
@@ -61,6 +63,7 @@ export class PowerSyncMongo {
|
|
|
61
63
|
this.locks = this.db.collection('locks');
|
|
62
64
|
this.bucket_state = this.db.collection('bucket_state');
|
|
63
65
|
this.checkpoint_events = this.db.collection('checkpoint_events');
|
|
66
|
+
this.connection_report_events = this.db.collection('connection_report_events');
|
|
64
67
|
}
|
|
65
68
|
|
|
66
69
|
/**
|
|
@@ -127,6 +130,34 @@ export class PowerSyncMongo {
|
|
|
127
130
|
max: 50 // max number of documents
|
|
128
131
|
});
|
|
129
132
|
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Only use in migrations and tests.
|
|
136
|
+
*/
|
|
137
|
+
async createConnectionReportingCollection() {
|
|
138
|
+
const existingCollections = await this.db
|
|
139
|
+
.listCollections({ name: 'connection_report_events' }, { nameOnly: false })
|
|
140
|
+
.toArray();
|
|
141
|
+
const collection = existingCollections[0];
|
|
142
|
+
if (collection != null) {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
await this.db.createCollection('connection_report_events');
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Only use in migrations and tests.
|
|
150
|
+
*/
|
|
151
|
+
async createBucketStateIndex() {
|
|
152
|
+
// TODO: Implement a better mechanism to use migrations in tests
|
|
153
|
+
await this.bucket_state.createIndex(
|
|
154
|
+
{
|
|
155
|
+
'_id.g': 1,
|
|
156
|
+
last_op: 1
|
|
157
|
+
},
|
|
158
|
+
{ name: 'bucket_updates', unique: true }
|
|
159
|
+
);
|
|
160
|
+
}
|
|
130
161
|
}
|
|
131
162
|
|
|
132
163
|
export function createPowerSyncMongo(config: MongoStorageConfig, options?: lib_mongo.MongoConnectionOptions) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { InternalOpId, storage } from '@powersync/service-core';
|
|
2
2
|
import { SqliteJsonValue } from '@powersync/service-sync-rules';
|
|
3
3
|
import * as bson from 'bson';
|
|
4
|
+
import { event_types } from '@powersync/service-types';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Replica id uniquely identifying a row on the source database.
|
|
@@ -97,6 +98,10 @@ export interface BucketStateDocument {
|
|
|
97
98
|
g: number;
|
|
98
99
|
b: string;
|
|
99
100
|
};
|
|
101
|
+
/**
|
|
102
|
+
* Important: There is an unique index on {'_id.g': 1, last_op: 1}.
|
|
103
|
+
* That means the last_op must match an actual op in the bucket, and not the commit checkpoint.
|
|
104
|
+
*/
|
|
100
105
|
last_op: bigint;
|
|
101
106
|
/**
|
|
102
107
|
* If set, this can be treated as "cache" of a checksum at a specific point.
|
|
@@ -234,3 +239,5 @@ export interface InstanceDocument {
|
|
|
234
239
|
// The instance UUID
|
|
235
240
|
_id: string;
|
|
236
241
|
}
|
|
242
|
+
|
|
243
|
+
export interface ClientConnectionDocument extends event_types.ClientConnection {}
|
|
@@ -7,8 +7,9 @@ export * from './implementation/MongoPersistedSyncRulesContent.js';
|
|
|
7
7
|
export * from './implementation/MongoStorageProvider.js';
|
|
8
8
|
export * from './implementation/MongoSyncBucketStorage.js';
|
|
9
9
|
export * from './implementation/MongoSyncRulesLock.js';
|
|
10
|
-
export * from './implementation/MongoTestStorageFactoryGenerator.js';
|
|
11
10
|
export * from './implementation/OperationBatch.js';
|
|
12
11
|
export * from './implementation/PersistedBatch.js';
|
|
13
|
-
export * from '
|
|
12
|
+
export * from '../utils/util.js';
|
|
14
13
|
export * from './MongoBucketStorage.js';
|
|
14
|
+
export * from './MongoReportStorage.js';
|
|
15
|
+
export * as test_utils from '../utils/test-utils.js';
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { mongo } from '@powersync/lib-service-mongodb';
|
|
2
|
+
import { PowerSyncMongo } from '../storage/implementation/db.js';
|
|
3
|
+
import { TestStorageOptions } from '@powersync/service-core';
|
|
4
|
+
import { MongoReportStorage } from '../storage/MongoReportStorage.js';
|
|
5
|
+
import { MongoBucketStorage } from '../storage/MongoBucketStorage.js';
|
|
6
|
+
import { MongoSyncBucketStorageOptions } from '../storage/implementation/MongoSyncBucketStorage.js';
|
|
7
|
+
|
|
8
|
+
export type MongoTestStorageOptions = {
|
|
9
|
+
url: string;
|
|
10
|
+
isCI: boolean;
|
|
11
|
+
internalOptions?: MongoSyncBucketStorageOptions;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export function mongoTestStorageFactoryGenerator(factoryOptions: MongoTestStorageOptions) {
|
|
15
|
+
return async (options?: TestStorageOptions) => {
|
|
16
|
+
const db = connectMongoForTests(factoryOptions.url, factoryOptions.isCI);
|
|
17
|
+
|
|
18
|
+
// None of the tests insert data into this collection, so it was never created
|
|
19
|
+
if (!(await db.db.listCollections({ name: db.bucket_parameters.collectionName }).hasNext())) {
|
|
20
|
+
await db.db.createCollection('bucket_parameters');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Full migrations are not currently run for tests, so we manually create this
|
|
24
|
+
await db.createCheckpointEventsCollection();
|
|
25
|
+
|
|
26
|
+
if (!options?.doNotClear) {
|
|
27
|
+
await db.clear();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return new MongoBucketStorage(db, { slot_name_prefix: 'test_' }, factoryOptions.internalOptions);
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function mongoTestReportStorageFactoryGenerator(factoryOptions: MongoTestStorageOptions) {
|
|
35
|
+
return async (options?: TestStorageOptions) => {
|
|
36
|
+
const db = connectMongoForTests(factoryOptions.url, factoryOptions.isCI);
|
|
37
|
+
|
|
38
|
+
await db.createConnectionReportingCollection();
|
|
39
|
+
|
|
40
|
+
if (!options?.doNotClear) {
|
|
41
|
+
await db.clear();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return new MongoReportStorage(db);
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export const connectMongoForTests = (url: string, isCI: boolean) => {
|
|
49
|
+
// Short timeout for tests, to fail fast when the server is not available.
|
|
50
|
+
// Slightly longer timeouts for CI, to avoid arbitrary test failures
|
|
51
|
+
const client = new mongo.MongoClient(url, {
|
|
52
|
+
connectTimeoutMS: isCI ? 15_000 : 5_000,
|
|
53
|
+
socketTimeoutMS: isCI ? 15_000 : 5_000,
|
|
54
|
+
serverSelectionTimeoutMS: isCI ? 15_000 : 2_500
|
|
55
|
+
});
|
|
56
|
+
return new PowerSyncMongo(client);
|
|
57
|
+
};
|
|
@@ -3,11 +3,9 @@ import * as crypto from 'crypto';
|
|
|
3
3
|
import * as uuid from 'uuid';
|
|
4
4
|
|
|
5
5
|
import { mongo } from '@powersync/lib-service-mongodb';
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
import { PowerSyncMongo } from './db.js';
|
|
9
|
-
import { BucketDataDocument } from './models.js';
|
|
6
|
+
import { storage, utils } from '@powersync/service-core';
|
|
10
7
|
import { ServiceAssertionError } from '@powersync/lib-services-framework';
|
|
8
|
+
import { BucketDataDocument } from '../storage/implementation/models.js';
|
|
11
9
|
|
|
12
10
|
export function idPrefixFilter<T>(prefix: Partial<T>, rest: (keyof T)[]): mongo.Condition<T> {
|
|
13
11
|
let filter = {
|
|
@@ -105,20 +103,6 @@ export function replicaIdToSubkey(table: bson.ObjectId, id: storage.ReplicaId):
|
|
|
105
103
|
}
|
|
106
104
|
}
|
|
107
105
|
|
|
108
|
-
/**
|
|
109
|
-
* Helper for unit tests
|
|
110
|
-
*/
|
|
111
|
-
export const connectMongoForTests = (url: string, isCI: boolean) => {
|
|
112
|
-
// Short timeout for tests, to fail fast when the server is not available.
|
|
113
|
-
// Slightly longer timeouts for CI, to avoid arbitrary test failures
|
|
114
|
-
const client = new mongo.MongoClient(url, {
|
|
115
|
-
connectTimeoutMS: isCI ? 15_000 : 5_000,
|
|
116
|
-
socketTimeoutMS: isCI ? 15_000 : 5_000,
|
|
117
|
-
serverSelectionTimeoutMS: isCI ? 15_000 : 2_500
|
|
118
|
-
});
|
|
119
|
-
return new PowerSyncMongo(client);
|
|
120
|
-
};
|
|
121
|
-
|
|
122
106
|
export function setSessionSnapshotTime(session: mongo.ClientSession, time: bson.Timestamp) {
|
|
123
107
|
// This is a workaround for the lack of direct support for snapshot reads in the MongoDB driver.
|
|
124
108
|
if (!session.snapshotEnabled) {
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
2
|
+
|
|
3
|
+
exports[`Connection reporting storage > Should create a connection report if its after a day 1`] = `
|
|
4
|
+
[
|
|
5
|
+
{
|
|
6
|
+
"client_id": "client_week",
|
|
7
|
+
"sdk": "powersync-js/1.24.5",
|
|
8
|
+
"user_agent": "powersync-js/1.21.0 powersync-web Firefox/141 linux",
|
|
9
|
+
"user_id": "user_week",
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"client_id": "client_week",
|
|
13
|
+
"sdk": "powersync-js/1.24.5",
|
|
14
|
+
"user_agent": "powersync-js/1.21.0 powersync-web Firefox/141 linux",
|
|
15
|
+
"user_id": "user_week",
|
|
16
|
+
},
|
|
17
|
+
]
|
|
18
|
+
`;
|
|
19
|
+
|
|
20
|
+
exports[`Connection reporting storage > Should delete rows older than specified range 1`] = `
|
|
21
|
+
{
|
|
22
|
+
"sdks": [
|
|
23
|
+
{
|
|
24
|
+
"clients": 1,
|
|
25
|
+
"sdk": "powersync-dart/1.6.4",
|
|
26
|
+
"users": 1,
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"clients": 1,
|
|
30
|
+
"sdk": "powersync-js/1.21.1",
|
|
31
|
+
"users": 1,
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"clients": 1,
|
|
35
|
+
"sdk": "powersync-js/1.21.2",
|
|
36
|
+
"users": 1,
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"clients": 1,
|
|
40
|
+
"sdk": "powersync-js/1.21.4",
|
|
41
|
+
"users": 1,
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"clients": 1,
|
|
45
|
+
"sdk": "powersync-js/1.24.5",
|
|
46
|
+
"users": 1,
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"clients": 1,
|
|
50
|
+
"sdk": "unknown",
|
|
51
|
+
"users": 1,
|
|
52
|
+
},
|
|
53
|
+
],
|
|
54
|
+
"users": 5,
|
|
55
|
+
}
|
|
56
|
+
`;
|
|
57
|
+
|
|
58
|
+
exports[`Connection reporting storage > Should update a connected connection report and make it disconnected 1`] = `
|
|
59
|
+
[
|
|
60
|
+
{
|
|
61
|
+
"client_id": "client_three",
|
|
62
|
+
"sdk": "powersync-js/1.21.2",
|
|
63
|
+
"user_agent": "powersync-js/1.21.0 powersync-web Firefox/141 linux",
|
|
64
|
+
"user_id": "user_three",
|
|
65
|
+
},
|
|
66
|
+
]
|
|
67
|
+
`;
|
|
68
|
+
|
|
69
|
+
exports[`Connection reporting storage > Should update a connection report if its within a day 1`] = `
|
|
70
|
+
[
|
|
71
|
+
{
|
|
72
|
+
"client_id": "client_one",
|
|
73
|
+
"sdk": "powersync-dart/1.6.4",
|
|
74
|
+
"user_agent": "powersync-dart/1.6.4 Dart (flutter-web) Chrome/128 android",
|
|
75
|
+
"user_id": "user_one",
|
|
76
|
+
},
|
|
77
|
+
]
|
|
78
|
+
`;
|
|
79
|
+
|
|
80
|
+
exports[`Report storage tests > Should show connection report data for user over the past day 1`] = `
|
|
81
|
+
{
|
|
82
|
+
"sdks": [
|
|
83
|
+
{
|
|
84
|
+
"clients": 1,
|
|
85
|
+
"sdk": "powersync-dart/1.6.4",
|
|
86
|
+
"users": 1,
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
"clients": 1,
|
|
90
|
+
"sdk": "powersync-js/1.21.1",
|
|
91
|
+
"users": 1,
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
"clients": 1,
|
|
95
|
+
"sdk": "powersync-js/1.21.4",
|
|
96
|
+
"users": 1,
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
"clients": 1,
|
|
100
|
+
"sdk": "unknown",
|
|
101
|
+
"users": 1,
|
|
102
|
+
},
|
|
103
|
+
],
|
|
104
|
+
"users": 3,
|
|
105
|
+
}
|
|
106
|
+
`;
|
|
107
|
+
|
|
108
|
+
exports[`Report storage tests > Should show connection report data for user over the past month 1`] = `
|
|
109
|
+
{
|
|
110
|
+
"sdks": [
|
|
111
|
+
{
|
|
112
|
+
"clients": 1,
|
|
113
|
+
"sdk": "powersync-dart/1.6.4",
|
|
114
|
+
"users": 1,
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
"clients": 1,
|
|
118
|
+
"sdk": "powersync-js/1.21.1",
|
|
119
|
+
"users": 1,
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
"clients": 1,
|
|
123
|
+
"sdk": "powersync-js/1.21.2",
|
|
124
|
+
"users": 1,
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
"clients": 1,
|
|
128
|
+
"sdk": "powersync-js/1.21.4",
|
|
129
|
+
"users": 1,
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
"clients": 1,
|
|
133
|
+
"sdk": "powersync-js/1.23.6",
|
|
134
|
+
"users": 1,
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
"clients": 1,
|
|
138
|
+
"sdk": "powersync-js/1.23.7",
|
|
139
|
+
"users": 1,
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
"clients": 1,
|
|
143
|
+
"sdk": "powersync-js/1.24.5",
|
|
144
|
+
"users": 1,
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
"clients": 1,
|
|
148
|
+
"sdk": "unknown",
|
|
149
|
+
"users": 1,
|
|
150
|
+
},
|
|
151
|
+
],
|
|
152
|
+
"users": 7,
|
|
153
|
+
}
|
|
154
|
+
`;
|
|
155
|
+
|
|
156
|
+
exports[`Report storage tests > Should show connection report data for user over the past week 1`] = `
|
|
157
|
+
{
|
|
158
|
+
"sdks": [
|
|
159
|
+
{
|
|
160
|
+
"clients": 1,
|
|
161
|
+
"sdk": "powersync-dart/1.6.4",
|
|
162
|
+
"users": 1,
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
"clients": 1,
|
|
166
|
+
"sdk": "powersync-js/1.21.1",
|
|
167
|
+
"users": 1,
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
"clients": 1,
|
|
171
|
+
"sdk": "powersync-js/1.21.2",
|
|
172
|
+
"users": 1,
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
"clients": 1,
|
|
176
|
+
"sdk": "powersync-js/1.21.4",
|
|
177
|
+
"users": 1,
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
"clients": 1,
|
|
181
|
+
"sdk": "powersync-js/1.24.5",
|
|
182
|
+
"users": 1,
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
"clients": 1,
|
|
186
|
+
"sdk": "unknown",
|
|
187
|
+
"users": 1,
|
|
188
|
+
},
|
|
189
|
+
],
|
|
190
|
+
"users": 5,
|
|
191
|
+
}
|
|
192
|
+
`;
|
|
193
|
+
|
|
194
|
+
exports[`Report storage tests > Should show currently connected users 1`] = `
|
|
195
|
+
{
|
|
196
|
+
"sdks": [
|
|
197
|
+
{
|
|
198
|
+
"clients": 1,
|
|
199
|
+
"sdk": "powersync-dart/1.6.4",
|
|
200
|
+
"users": 1,
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
"clients": 1,
|
|
204
|
+
"sdk": "powersync-js/1.21.1",
|
|
205
|
+
"users": 1,
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
"clients": 1,
|
|
209
|
+
"sdk": "unknown",
|
|
210
|
+
"users": 1,
|
|
211
|
+
},
|
|
212
|
+
],
|
|
213
|
+
"users": 2,
|
|
214
|
+
}
|
|
215
|
+
`;
|
|
@@ -1,6 +1,22 @@
|
|
|
1
1
|
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
2
2
|
|
|
3
|
-
exports[`Mongo Sync Bucket Storage > empty storage metrics 1`] = `
|
|
3
|
+
exports[`Mongo Sync Bucket Storage - Data > empty storage metrics 1`] = `
|
|
4
|
+
{
|
|
5
|
+
"operations_size_bytes": 0,
|
|
6
|
+
"parameters_size_bytes": 0,
|
|
7
|
+
"replication_size_bytes": 0,
|
|
8
|
+
}
|
|
9
|
+
`;
|
|
10
|
+
|
|
11
|
+
exports[`Mongo Sync Bucket Storage - split buckets > empty storage metrics 1`] = `
|
|
12
|
+
{
|
|
13
|
+
"operations_size_bytes": 0,
|
|
14
|
+
"parameters_size_bytes": 0,
|
|
15
|
+
"replication_size_bytes": 0,
|
|
16
|
+
}
|
|
17
|
+
`;
|
|
18
|
+
|
|
19
|
+
exports[`Mongo Sync Bucket Storage - split operations > empty storage metrics 1`] = `
|
|
4
20
|
{
|
|
5
21
|
"operations_size_bytes": 0,
|
|
6
22
|
"parameters_size_bytes": 0,
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
|
2
|
+
import { INITIALIZED_MONGO_REPORT_STORAGE_FACTORY } from './util.js';
|
|
3
|
+
import { register, ReportUserData } from '@powersync/service-core-tests';
|
|
4
|
+
import { event_types } from '@powersync/service-types';
|
|
5
|
+
import { MongoReportStorage } from '@module/storage/MongoReportStorage.js';
|
|
6
|
+
|
|
7
|
+
const userData = register.REPORT_TEST_USERS;
|
|
8
|
+
const dates = register.REPORT_TEST_DATES;
|
|
9
|
+
const factory = await INITIALIZED_MONGO_REPORT_STORAGE_FACTORY();
|
|
10
|
+
|
|
11
|
+
function removeVolatileFields(
|
|
12
|
+
connections: event_types.ClientConnection[]
|
|
13
|
+
): Partial<event_types.ClientConnection & { _id: string }>[] {
|
|
14
|
+
return connections.map((sdk: Partial<event_types.ClientConnection & { _id: string }>) => {
|
|
15
|
+
const { _id, disconnected_at, connected_at, jwt_exp, ...rest } = sdk;
|
|
16
|
+
return {
|
|
17
|
+
...rest
|
|
18
|
+
};
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function loadData(data: ReportUserData, factory: MongoReportStorage) {
|
|
23
|
+
await factory.db.connection_report_events.insertMany(Object.values(data));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function deleteData(factory: MongoReportStorage) {
|
|
27
|
+
await factory.db.connection_report_events.deleteMany();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
beforeAll(async () => {
|
|
31
|
+
await loadData(userData, factory);
|
|
32
|
+
});
|
|
33
|
+
afterAll(async () => {
|
|
34
|
+
await deleteData(factory);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe('Report storage tests', async () => {
|
|
38
|
+
await register.registerReportTests(factory);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe('Connection reporting storage', async () => {
|
|
42
|
+
it('Should create a connection report if its after a day', async () => {
|
|
43
|
+
const newConnectAt = new Date(
|
|
44
|
+
dates.now.getFullYear(),
|
|
45
|
+
dates.now.getMonth(),
|
|
46
|
+
dates.now.getDate() + 1,
|
|
47
|
+
dates.now.getHours()
|
|
48
|
+
);
|
|
49
|
+
const jwtExp = new Date(newConnectAt.getFullYear(), newConnectAt.getMonth(), newConnectAt.getDate() + 1);
|
|
50
|
+
|
|
51
|
+
await factory.reportClientConnection({
|
|
52
|
+
sdk: userData.user_week.sdk,
|
|
53
|
+
connected_at: newConnectAt,
|
|
54
|
+
jwt_exp: jwtExp,
|
|
55
|
+
client_id: userData.user_week.client_id,
|
|
56
|
+
user_id: userData.user_week.user_id,
|
|
57
|
+
user_agent: userData.user_week.user_agent
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const connection = await factory.db.connection_report_events.find({ user_id: userData.user_week.user_id }).toArray();
|
|
61
|
+
expect(connection).toHaveLength(2);
|
|
62
|
+
const cleaned = removeVolatileFields(connection);
|
|
63
|
+
expect(cleaned).toMatchSnapshot();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('Should update a connection report if its within a day', async () => {
|
|
67
|
+
const newConnectAt = new Date(
|
|
68
|
+
dates.now.getFullYear(),
|
|
69
|
+
dates.now.getMonth(),
|
|
70
|
+
dates.now.getDate(),
|
|
71
|
+
dates.now.getHours(),
|
|
72
|
+
dates.now.getMinutes() + 20
|
|
73
|
+
);
|
|
74
|
+
const jwtExp = new Date(newConnectAt.getFullYear(), newConnectAt.getMonth(), newConnectAt.getDate() + 1);
|
|
75
|
+
await factory.reportClientConnection({
|
|
76
|
+
sdk: userData.user_one.sdk,
|
|
77
|
+
connected_at: newConnectAt,
|
|
78
|
+
jwt_exp: jwtExp,
|
|
79
|
+
client_id: userData.user_one.client_id,
|
|
80
|
+
user_id: userData.user_one.user_id,
|
|
81
|
+
user_agent: userData.user_one.user_agent
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const connection = await factory.db.connection_report_events
|
|
85
|
+
.find({ user_id: userData.user_one.user_id, client_id: userData.user_one.client_id })
|
|
86
|
+
.toArray();
|
|
87
|
+
expect(connection).toHaveLength(1);
|
|
88
|
+
expect(new Date(connection[0].connected_at)).toEqual(newConnectAt);
|
|
89
|
+
expect(new Date(connection[0].jwt_exp!)).toEqual(jwtExp);
|
|
90
|
+
expect(connection[0].disconnected_at).toBeUndefined();
|
|
91
|
+
const cleaned = removeVolatileFields(connection);
|
|
92
|
+
expect(cleaned).toMatchSnapshot();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('Should update a connected connection report and make it disconnected', async () => {
|
|
96
|
+
const disconnectAt = new Date(
|
|
97
|
+
dates.now.getFullYear(),
|
|
98
|
+
dates.now.getMonth(),
|
|
99
|
+
dates.now.getDate(),
|
|
100
|
+
dates.now.getHours(),
|
|
101
|
+
dates.now.getMinutes() + 20
|
|
102
|
+
);
|
|
103
|
+
const jwtExp = new Date(disconnectAt.getFullYear(), disconnectAt.getMonth(), disconnectAt.getDate() + 1);
|
|
104
|
+
|
|
105
|
+
await factory.reportClientDisconnection({
|
|
106
|
+
disconnected_at: disconnectAt,
|
|
107
|
+
jwt_exp: jwtExp,
|
|
108
|
+
client_id: userData.user_three.client_id,
|
|
109
|
+
user_id: userData.user_three.user_id,
|
|
110
|
+
user_agent: userData.user_three.user_agent,
|
|
111
|
+
connected_at: userData.user_three.connected_at
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
const connection = await factory.db.connection_report_events.find({ user_id: userData.user_three.user_id }).toArray();
|
|
115
|
+
expect(connection).toHaveLength(1);
|
|
116
|
+
expect(new Date(connection[0].disconnected_at!)).toEqual(disconnectAt);
|
|
117
|
+
const cleaned = removeVolatileFields(connection);
|
|
118
|
+
expect(cleaned).toMatchSnapshot();
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('Should delete rows older than specified range', async () => {
|
|
122
|
+
await deleteData(factory);
|
|
123
|
+
await loadData(userData, factory);
|
|
124
|
+
await factory.deleteOldConnectionData({
|
|
125
|
+
date: dates.weekAgo
|
|
126
|
+
});
|
|
127
|
+
const connection = await factory.getClientConnectionReports({
|
|
128
|
+
start: dates.monthAgo,
|
|
129
|
+
end: dates.now
|
|
130
|
+
});
|
|
131
|
+
expect(connection).toMatchSnapshot();
|
|
132
|
+
});
|
|
133
|
+
});
|