@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.
- package/CHANGELOG.md +9 -7
- package/dist/.tsbuildinfo +1 -1
- package/dist/@types/storage/PostgresReportStorageFactory.d.ts +5 -1
- package/dist/migrations/scripts/1684951997326-init.js +17 -0
- package/dist/migrations/scripts/1684951997326-init.js.map +1 -1
- package/dist/storage/PostgresReportStorageFactory.js +260 -11
- package/dist/storage/PostgresReportStorageFactory.js.map +1 -1
- package/dist/storage/PostgresStorageProvider.js +5 -2
- package/dist/storage/PostgresStorageProvider.js.map +1 -1
- package/package.json +7 -7
- package/src/migrations/scripts/1684951997326-init.ts +22 -0
- package/src/storage/PostgresReportStorageFactory.ts +266 -13
- package/src/storage/PostgresStorageProvider.ts +5 -2
|
@@ -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
|
-
|
|
38
|
-
|
|
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
|
-
|
|
41
|
-
|
|
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
|
-
|
|
44
|
-
|
|
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
|
-
|
|
47
|
-
|
|
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
|
-
|
|
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 () =>
|
|
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;
|