@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.
Files changed (76) hide show
  1. package/CHANGELOG.md +31 -7
  2. package/dist/index.d.ts +1 -0
  3. package/dist/index.js +1 -0
  4. package/dist/index.js.map +1 -1
  5. package/dist/migrations/db/migrations/1741697235857-bucket-state-index.js +1 -4
  6. package/dist/migrations/db/migrations/1741697235857-bucket-state-index.js.map +1 -1
  7. package/dist/migrations/db/migrations/1752661449910-connection-reporting.d.ts +3 -0
  8. package/dist/migrations/db/migrations/1752661449910-connection-reporting.js +36 -0
  9. package/dist/migrations/db/migrations/1752661449910-connection-reporting.js.map +1 -0
  10. package/dist/storage/MongoBucketStorage.d.ts +3 -2
  11. package/dist/storage/MongoBucketStorage.js +5 -3
  12. package/dist/storage/MongoBucketStorage.js.map +1 -1
  13. package/dist/storage/MongoReportStorage.d.ts +17 -0
  14. package/dist/storage/MongoReportStorage.js +152 -0
  15. package/dist/storage/MongoReportStorage.js.map +1 -0
  16. package/dist/storage/implementation/MongoBucketBatch.js +1 -1
  17. package/dist/storage/implementation/MongoBucketBatch.js.map +1 -1
  18. package/dist/storage/implementation/MongoChecksums.d.ts +45 -13
  19. package/dist/storage/implementation/MongoChecksums.js +148 -135
  20. package/dist/storage/implementation/MongoChecksums.js.map +1 -1
  21. package/dist/storage/implementation/MongoCompactor.js +23 -21
  22. package/dist/storage/implementation/MongoCompactor.js.map +1 -1
  23. package/dist/storage/implementation/MongoStorageProvider.d.ts +1 -1
  24. package/dist/storage/implementation/MongoStorageProvider.js +7 -3
  25. package/dist/storage/implementation/MongoStorageProvider.js.map +1 -1
  26. package/dist/storage/implementation/MongoSyncBucketStorage.d.ts +5 -2
  27. package/dist/storage/implementation/MongoSyncBucketStorage.js +4 -4
  28. package/dist/storage/implementation/MongoSyncBucketStorage.js.map +1 -1
  29. package/dist/storage/implementation/PersistedBatch.js +1 -1
  30. package/dist/storage/implementation/PersistedBatch.js.map +1 -1
  31. package/dist/storage/implementation/db.d.ts +10 -1
  32. package/dist/storage/implementation/db.js +25 -0
  33. package/dist/storage/implementation/db.js.map +1 -1
  34. package/dist/storage/implementation/models.d.ts +7 -0
  35. package/dist/storage/storage-index.d.ts +3 -2
  36. package/dist/storage/storage-index.js +3 -2
  37. package/dist/storage/storage-index.js.map +1 -1
  38. package/dist/utils/test-utils.d.ts +13 -0
  39. package/dist/utils/test-utils.js +40 -0
  40. package/dist/utils/test-utils.js.map +1 -0
  41. package/dist/{storage/implementation → utils}/util.d.ts +1 -6
  42. package/dist/{storage/implementation → utils}/util.js +0 -15
  43. package/dist/utils/util.js.map +1 -0
  44. package/dist/utils/utils-index.d.ts +2 -0
  45. package/dist/utils/utils-index.js +3 -0
  46. package/dist/utils/utils-index.js.map +1 -0
  47. package/package.json +7 -7
  48. package/src/index.ts +1 -0
  49. package/src/migrations/db/migrations/1741697235857-bucket-state-index.ts +1 -7
  50. package/src/migrations/db/migrations/1752661449910-connection-reporting.ts +58 -0
  51. package/src/storage/MongoBucketStorage.ts +5 -4
  52. package/src/storage/MongoReportStorage.ts +174 -0
  53. package/src/storage/implementation/MongoBucketBatch.ts +1 -1
  54. package/src/storage/implementation/MongoChecksums.ts +172 -150
  55. package/src/storage/implementation/MongoCompactor.ts +23 -22
  56. package/src/storage/implementation/MongoStorageProvider.ts +9 -4
  57. package/src/storage/implementation/MongoSyncBucketStorage.ts +11 -5
  58. package/src/storage/implementation/PersistedBatch.ts +1 -1
  59. package/src/storage/implementation/db.ts +31 -0
  60. package/src/storage/implementation/models.ts +7 -0
  61. package/src/storage/storage-index.ts +3 -2
  62. package/src/utils/test-utils.ts +57 -0
  63. package/src/{storage/implementation → utils}/util.ts +2 -18
  64. package/src/utils/utils-index.ts +2 -0
  65. package/test/src/__snapshots__/connection-report-storage.test.ts.snap +215 -0
  66. package/test/src/__snapshots__/storage.test.ts.snap +17 -1
  67. package/test/src/connection-report-storage.test.ts +133 -0
  68. package/test/src/storage.test.ts +38 -1
  69. package/test/src/storage_compacting.test.ts +120 -5
  70. package/test/src/util.ts +6 -2
  71. package/tsconfig.tsbuildinfo +1 -1
  72. package/dist/storage/implementation/MongoTestStorageFactoryGenerator.d.ts +0 -7
  73. package/dist/storage/implementation/MongoTestStorageFactoryGenerator.js +0 -18
  74. package/dist/storage/implementation/MongoTestStorageFactoryGenerator.js.map +0 -1
  75. package/dist/storage/implementation/util.js.map +0 -1
  76. 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.BucketStorageProvider {
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 factory = new MongoBucketStorage(database, {
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: factory,
49
+ storage: syncStorageFactory,
50
+ reportStorage: reportStorageFactory,
46
51
  shutDown: async () => {
47
52
  shuttingDown = true;
48
- await factory[Symbol.asyncDispose]();
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 './util.js';
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: storage.WriteCheckpointMode = storage.WriteCheckpointMode.MANAGED
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
  }
@@ -16,7 +16,7 @@ import {
16
16
  CurrentDataDocument,
17
17
  SourceKey
18
18
  } from './models.js';
19
- import { replicaIdToSubkey } from './util.js';
19
+ import { replicaIdToSubkey } from '../../utils/util.js';
20
20
 
21
21
  /**
22
22
  * Maximum size of operations we write in a single transaction.
@@ -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 './implementation/util.js';
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 { BucketChecksum, PartialChecksum, PartialOrFullChecksum, storage, utils } from '@powersync/service-core';
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,2 @@
1
+ export * as test_utils from './test-utils.js';
2
+ export * from './util.js';
@@ -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
+ });