@powersync/service-module-postgres-storage 0.0.0-dev-20250724111728 → 0.0.0-dev-20250729121434

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,6 +1,7 @@
1
1
  import { storage } from '@powersync/service-core';
2
2
  import * as pg_wire from '@powersync/service-jpgwire';
3
-
3
+ import { event_types } from '@powersync/service-types';
4
+ import { v4 } from 'uuid';
4
5
  import * as lib_postgres from '@powersync/lib-service-postgres';
5
6
  import { NormalizedPostgresStorageConfig } from '../types/types.js';
6
7
 
@@ -21,8 +22,6 @@ export type PostgresReportStorageOptions = {
21
22
 
22
23
  export class PostgresReportStorageFactory implements storage.ReportStorageFactory {
23
24
  readonly db: lib_postgres.DatabaseClient;
24
- private activeStorage: storage.SyncRulesBucketStorage | undefined;
25
-
26
25
  constructor(protected options: PostgresReportStorageOptions) {
27
26
  this.db = new lib_postgres.DatabaseClient({
28
27
  config: options.config,
@@ -34,20 +33,274 @@ export class PostgresReportStorageFactory implements storage.ReportStorageFactor
34
33
  connectionCreated: async (connection) => this.prepareStatements(connection)
35
34
  });
36
35
  }
37
- reportSdkConnect(data: SdkConnectBucketData): Promise<void> {
38
- throw new Error('Method not implemented.');
36
+
37
+ private parseJsDate(date: Date) {
38
+ const year = date.getFullYear();
39
+ const month = date.getMonth();
40
+ const today = date.getDate();
41
+ const day = date.getDay();
42
+ return {
43
+ year,
44
+ month,
45
+ today,
46
+ day,
47
+ parsedDate: date
48
+ };
49
+ }
50
+ private timeFrameQuery(timeframe: event_types.TimeFrames, interval: number = 1) {
51
+ const { year, month, today, parsedDate } = this.parseJsDate(new Date());
52
+ switch (timeframe) {
53
+ case 'month': {
54
+ return { lt: parsedDate.toISOString(), gt: new Date(year, parsedDate.getMonth() - interval).toISOString() };
55
+ }
56
+ case 'week': {
57
+ const weekStartDate = new Date(parsedDate);
58
+ weekStartDate.setDate(weekStartDate.getDate() - 6 * interval);
59
+ const weekStart = this.parseJsDate(weekStartDate);
60
+ return {
61
+ lt: parsedDate.toISOString(),
62
+ gt: new Date(weekStart.year, weekStart.month, weekStart.today).toISOString()
63
+ };
64
+ }
65
+ case 'hour': {
66
+ // Get the last hour from the current time
67
+ const previousHour = parsedDate.getHours() - interval;
68
+ return {
69
+ lt: new Date(year, month, today, parsedDate.getHours()).toISOString(),
70
+ gt: new Date(year, month, today, previousHour).toISOString()
71
+ };
72
+ }
73
+ default: {
74
+ return {
75
+ lt: parsedDate.toISOString(),
76
+ gt: new Date(year, month, today - interval).toISOString()
77
+ };
78
+ }
79
+ }
80
+ }
81
+
82
+ private timeFrameDeleteQuery(timeframe: event_types.TimeFrames, interval: number = 1) {
83
+ const { year, month, today, parsedDate } = this.parseJsDate(new Date());
84
+ switch (timeframe) {
85
+ case 'month': {
86
+ return { lt: new Date(year, parsedDate.getMonth() - interval).toISOString() };
87
+ }
88
+ case 'week': {
89
+ const weekStartDate = new Date(parsedDate);
90
+ weekStartDate.setDate(weekStartDate.getDate() - 6 * interval);
91
+ const { month, year, today } = this.parseJsDate(weekStartDate);
92
+ return {
93
+ lt: new Date(year, month, today).toISOString()
94
+ };
95
+ }
96
+ case 'hour': {
97
+ const previousHour = parsedDate.getHours() - interval;
98
+ return {
99
+ lt: new Date(year, month, today, previousHour).toISOString()
100
+ };
101
+ }
102
+ default: {
103
+ return {
104
+ $lt: new Date(year, month, today - interval).toISOString()
105
+ };
106
+ }
107
+ }
108
+ }
109
+
110
+ private listConnectionsDateRangeQuery(data: event_types.ListCurrentConnectionsRequest) {
111
+ const { range } = data;
112
+ if (!range) {
113
+ const query = `
114
+ WITH filtered AS (
115
+ SELECT *
116
+ FROM sdk_report_events
117
+ WHERE disconnect_at IS NULL
118
+ AND jwt_exp > NOW()
119
+ ),
120
+ unique_users AS (
121
+ SELECT COUNT(DISTINCT user_id) AS count
122
+ FROM filtered
123
+ ),
124
+ sdk_versions_array AS (
125
+ SELECT sdk,
126
+ COUNT(*) AS total,
127
+ COUNT(DISTINCT client_id) AS clients,
128
+ COUNT(DISTINCT user_id) AS users
129
+ FROM filtered
130
+ GROUP BY sdk
131
+ )
132
+ SELECT
133
+ COALESCE(u.count, 0) AS users,
134
+ JSON_AGG(ROW_TO_JSON(s)) AS sdks
135
+ FROM unique_users u
136
+ JOIN sdk_versions_array s ON TRUE;
137
+ `;
138
+ return {
139
+ statement: query
140
+ };
141
+ }
142
+ const endDate = data.range?.end_date ? new Date(data.range.end_date) : new Date();
143
+ const startDate = new Date(range.start_date);
144
+ const query = `
145
+ WITH filtered AS (
146
+ SELECT *
147
+ FROM sdk_report_events
148
+ WHERE disconnect_at IS NULL
149
+ AND jwt_exp > NOW()
150
+ AND connect_at > $1
151
+ AND connect_at <= $2
152
+ ),
153
+ unique_users AS (
154
+ SELECT COUNT(DISTINCT user_id) AS count
155
+ FROM filtered
156
+ ),
157
+ sdk_versions_array AS (
158
+ SELECT sdk,
159
+ COUNT(*) AS total,
160
+ COUNT(DISTINCT client_id) AS clients,
161
+ COUNT(DISTINCT user_id) AS users
162
+ FROM filtered
163
+ GROUP BY sdk
164
+ )
165
+ SELECT COALESCE(u.count, 0) AS users, JSON_AGG(ROW_TO_JSON(s)) AS sdks
166
+ FROM unique_users u
167
+ JOIN sdk_versions_array s ON TRUE;
168
+ `;
169
+ const lt = endDate.toISOString();
170
+ const gt = startDate.toISOString();
171
+ return { statement: query, params: [{ value: gt }, { value: lt }] };
39
172
  }
40
- reportSdkDisconnect(data: SdkDisconnectEventData): Promise<void> {
41
- throw new Error('Method not implemented.');
173
+
174
+ private updateTableFilter() {
175
+ const { year, month, today } = this.parseJsDate(new Date());
176
+ const nextDay = today + 1;
177
+ return {
178
+ gte: new Date(year, month, today).toISOString(),
179
+ lt: new Date(year, month, nextDay).toISOString()
180
+ };
42
181
  }
43
- listCurrentConnections(data: ListCurrentConnectionsRequest): Promise<ListCurrentConnections> {
44
- throw new Error('Method not implemented.');
182
+
183
+ async reportSdkConnect(data: SdkConnectBucketData): Promise<void> {
184
+ const { sdk, connect_at, user_id, user_agent, jwt_exp, client_id } = data;
185
+ const { gte, lt } = this.updateTableFilter();
186
+ const query = `
187
+ INSERT INTO sdk_report_events (user_id, client_id, connect_at, sdk, user_agent, jwt_exp, id)
188
+ VALUES ($1, $2, $3, $4, $5, $6, $7)
189
+ ON CONFLICT (user_id, client_id, connect_at)
190
+ DO UPDATE SET
191
+ connect_at = $3,
192
+ sdk = $4,
193
+ user_agent = $5,
194
+ jwt_exp = $6,
195
+ disconnect_at = NULL
196
+ WHERE sdk_report_events.connect_at >= $8
197
+ AND sdk_report_events.connect_at < $9;`;
198
+ try {
199
+ const result = await this.db.query({
200
+ statement: query,
201
+ params: [
202
+ { type: 'varchar', value: user_id },
203
+ { type: 'varchar', value: client_id },
204
+ { type: 'varchar', value: connect_at },
205
+ { type: 'varchar', value: sdk },
206
+ { type: 'varchar', value: user_agent },
207
+ { type: 1184, value: jwt_exp?.toISOString() },
208
+ { type: 'varchar', value: v4() },
209
+ { type: 1184, value: gte },
210
+ { type: 1184, value: lt }
211
+ ]
212
+ });
213
+ console.log(result.rows);
214
+ } catch (error) {
215
+ console.log(error);
216
+ }
45
217
  }
46
- scrapeSdkData(data: ScrapeSdkDataRequest): Promise<ListCurrentConnections> {
47
- throw new Error('Method not implemented.');
218
+ async reportSdkDisconnect(data: SdkDisconnectEventData): Promise<void> {
219
+ const { user_id, client_id, disconnect_at } = data;
220
+ const { gte, lt } = this.updateTableFilter();
221
+ const query = `
222
+ UPDATE sdk_report_events
223
+ SET
224
+ disconnect_at = $1,
225
+ jwt_exp = NULL
226
+ WHERE user_id = $2
227
+ AND client_id = $3
228
+ AND connect_at >= $4
229
+ AND connect_at < $5;`;
230
+
231
+ try {
232
+ const result = await this.db.query({
233
+ statement: query,
234
+ params: [
235
+ { type: 1184, value: disconnect_at },
236
+ { type: 'varchar', value: user_id },
237
+ { type: 'varchar', value: client_id },
238
+ { type: 1184, value: gte },
239
+ { type: 1184, value: lt }
240
+ ]
241
+ });
242
+ console.log(result.rows);
243
+ } catch (error) {
244
+ console.log(error);
245
+ }
246
+ }
247
+ async listCurrentConnections(data: ListCurrentConnectionsRequest): Promise<ListCurrentConnections> {
248
+ const statement = this.listConnectionsDateRangeQuery(data);
249
+ const result = await this.db.query(statement);
250
+ console.log(result.rows);
251
+ return {
252
+ users: 0,
253
+ sdks: []
254
+ };
255
+ }
256
+
257
+ async scrapeSdkData(data: ScrapeSdkDataRequest): Promise<ListCurrentConnections> {
258
+ const { timeframe, interval } = data;
259
+ const { lt, gt } = this.timeFrameQuery(timeframe, interval);
260
+ const query = `
261
+ WITH filtered AS (
262
+ SELECT *
263
+ FROM sdk_report_events
264
+ WHERE connect_at > $1
265
+ AND connect_at <= $2
266
+ ),
267
+ unique_users AS (
268
+ SELECT COUNT(DISTINCT user_id) AS count
269
+ FROM filtered
270
+ ),
271
+ sdk_versions_array AS (
272
+ SELECT sdk,
273
+ COUNT(*) AS total,
274
+ COUNT(DISTINCT client_id) AS clients,
275
+ COUNT(DISTINCT user_id) AS users
276
+ FROM filtered
277
+ GROUP BY sdk
278
+ )
279
+ SELECT COALESCE(u.count, 0) AS users, JSON_AGG(ROW_TO_JSON(s)) AS sdks
280
+ FROM unique_users u
281
+ JOIN sdk_versions_array s ON TRUE;
282
+ `;
283
+ const result = await this.db.query({ statement: query, params: [{ value: gt }, { value: lt }] });
284
+ console.log(result.rows);
285
+ return {
286
+ users: 0,
287
+ sdks: []
288
+ };
48
289
  }
49
- deleteOldSdkData(data: DeleteOldSdkData): Promise<void> {
50
- throw new Error('Method not implemented.');
290
+ async deleteOldSdkData(data: DeleteOldSdkData): Promise<void> {
291
+ const { timeframe, interval } = data;
292
+ const { lt } = this.timeFrameDeleteQuery(timeframe, interval);
293
+ const query = `
294
+ DELETE FROM sdk_report_events
295
+ WHERE connect_at < $1
296
+ AND (
297
+ disconnect_at IS NOT NULL
298
+ OR (jwt_exp < NOW() AND disconnect_at IS NULL)
299
+ );
300
+ `;
301
+ const params = [{ value: lt }];
302
+ const result = await this.db.query({ statement: query, params });
303
+ console.log(result.rows);
51
304
  }
52
305
 
53
306
  async [Symbol.asyncDispose]() {
@@ -35,14 +35,17 @@ export class PostgresStorageProvider implements storage.BucketStorageProvider {
35
35
  });
36
36
 
37
37
  return {
38
- // TODO: IMPLEMENT REPORT STORAGE
39
38
  reportStorage: reportStorageFactory,
40
39
  storage: storageFactory,
41
- shutDown: async () => storageFactory.db[Symbol.asyncDispose](),
40
+ shutDown: async () => {
41
+ await storageFactory.db[Symbol.asyncDispose]();
42
+ await reportStorageFactory.db[Symbol.asyncDispose]();
43
+ },
42
44
  tearDown: async () => {
43
45
  logger.info(`Tearing down Postgres storage: ${normalizedConfig.database}...`);
44
46
  await dropTables(storageFactory.db);
45
47
  await storageFactory.db[Symbol.asyncDispose]();
48
+ await reportStorageFactory.db[Symbol.asyncDispose]();
46
49
  return true;
47
50
  }
48
51
  } satisfies storage.ActiveStorage;