@hot-updater/server 1.0.0-rc.0 → 1.0.0-rc.2
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/dist/_virtual/_rolldown/runtime.cjs +1 -1
- package/dist/adapters/databaseConstraintErrors.cjs +0 -1
- package/dist/adapters/databasePluginUtils.cjs +0 -1
- package/dist/adapters/drizzle.cjs +11 -4
- package/dist/adapters/drizzle.d.cts +0 -1
- package/dist/adapters/drizzle.d.mts +0 -1
- package/dist/adapters/drizzle.mjs +12 -4
- package/dist/adapters/drizzleCrud.cjs +62 -1
- package/dist/adapters/drizzleCrud.mjs +61 -2
- package/dist/adapters/drizzleLazyDB.cjs +6 -1
- package/dist/adapters/drizzleLazyDB.mjs +6 -1
- package/dist/adapters/drizzleQuery.cjs +3 -2
- package/dist/adapters/drizzleQuery.mjs +3 -1
- package/dist/adapters/insightsTransaction.cjs +22 -0
- package/dist/adapters/insightsTransaction.mjs +22 -0
- package/dist/adapters/kysely.cjs +2 -1
- package/dist/adapters/kysely.d.cts +0 -1
- package/dist/adapters/kysely.d.mts +0 -1
- package/dist/adapters/kysely.mjs +3 -1
- package/dist/adapters/kyselyCrud.cjs +54 -3
- package/dist/adapters/kyselyCrud.mjs +54 -3
- package/dist/adapters/kyselyQuery.cjs +2 -2
- package/dist/adapters/kyselyQuery.mjs +2 -1
- package/dist/adapters/mongodb.cjs +27 -1
- package/dist/adapters/mongodb.d.cts +1 -1
- package/dist/adapters/mongodb.d.mts +1 -1
- package/dist/adapters/mongodb.mjs +28 -1
- package/dist/adapters/mongodbCollections.cjs +2 -0
- package/dist/adapters/mongodbCollections.mjs +2 -0
- package/dist/adapters/mongodbQuery.cjs +21 -4
- package/dist/adapters/mongodbQuery.mjs +21 -5
- package/dist/adapters/mongodbReads.cjs +91 -29
- package/dist/adapters/mongodbReads.mjs +92 -29
- package/dist/adapters/mongodbWrites.cjs +16 -2
- package/dist/adapters/mongodbWrites.mjs +18 -3
- package/dist/adapters/prisma.cjs +55 -7
- package/dist/adapters/prisma.d.cts +0 -1
- package/dist/adapters/prisma.d.mts +0 -1
- package/dist/adapters/prisma.mjs +56 -7
- package/dist/adapters/prismaQuery.cjs +0 -1
- package/dist/adapters/prismaRows.cjs +22 -1
- package/dist/adapters/prismaRows.mjs +22 -1
- package/dist/apiKeys.cjs +6 -2
- package/dist/apiKeys.d.cts +0 -1
- package/dist/apiKeys.d.mts +0 -1
- package/dist/apiKeys.mjs +3 -3
- package/dist/createHotUpdaterCore.cjs +27 -12
- package/dist/createHotUpdaterCore.d.cts +8 -8
- package/dist/createHotUpdaterCore.d.mts +8 -8
- package/dist/createHotUpdaterCore.mjs +26 -12
- package/dist/db/createBundleDiff.cjs +2 -2
- package/dist/db/createBundleDiff.d.cts +1 -5
- package/dist/db/createBundleDiff.d.mts +1 -5
- package/dist/db/databasePluginCore.cjs +0 -1
- package/dist/db/fixedMigrator.cjs +2 -2
- package/dist/db/fixedMigrator.mjs +2 -2
- package/dist/db/fixedMigratorMongo.cjs +6 -4
- package/dist/db/fixedMigratorMongo.mjs +6 -4
- package/dist/db/index.d.cts +0 -1
- package/dist/db/index.d.mts +0 -1
- package/dist/db/releaseCatalog.cjs +0 -1
- package/dist/db/releaseCatalog.d.cts +0 -1
- package/dist/db/releaseCatalog.d.mts +0 -1
- package/dist/db/schema/insightsCollation.cjs +14 -0
- package/dist/db/schema/insightsCollation.mjs +14 -0
- package/dist/db/schema/sql.cjs +11 -7
- package/dist/db/schema/sql.mjs +8 -8
- package/dist/db/schemaGenerators.cjs +28 -9
- package/dist/db/schemaGenerators.mjs +28 -9
- package/dist/db/schemaReadiness.cjs +2 -0
- package/dist/db/schemaReadiness.d.cts +1 -0
- package/dist/db/schemaReadiness.d.mts +1 -0
- package/dist/db/schemaReadiness.mjs +2 -0
- package/dist/db/sqlMigrationExecution.cjs +1 -1
- package/dist/db/sqlMigrationExecution.mjs +1 -0
- package/dist/db/types.cjs +3 -1
- package/dist/db/types.d.cts +3 -6
- package/dist/db/types.d.mts +3 -6
- package/dist/db/types.mjs +2 -2
- package/dist/db/updateArtifacts.cjs +0 -1
- package/dist/handler.cjs +5 -5
- package/dist/handler.d.cts +1 -2
- package/dist/handler.d.mts +1 -2
- package/dist/handler.mjs +5 -5
- package/dist/handlerReleaseCatalogRoutes.cjs +0 -1
- package/dist/handlerReleaseManagementRoutes.cjs +0 -1
- package/dist/handlerTypes.d.cts +0 -1
- package/dist/handlerTypes.d.mts +0 -1
- package/dist/handlerVersionRoutes.d.cts +1 -0
- package/dist/handlerVersionRoutes.d.mts +1 -0
- package/dist/index.cjs +2 -2
- package/dist/index.d.cts +4 -4
- package/dist/index.d.mts +4 -4
- package/dist/index.mjs +2 -2
- package/dist/insights/domain.d.cts +89 -0
- package/dist/insights/domain.d.mts +89 -0
- package/dist/insights/errors.cjs +15 -0
- package/dist/insights/errors.mjs +14 -0
- package/dist/{analytics → insights}/eventInput.cjs +58 -14
- package/dist/{analytics → insights}/eventInput.mjs +58 -16
- package/dist/insights/provider.cjs +314 -0
- package/dist/insights/provider.d.cts +6 -0
- package/dist/insights/provider.d.mts +6 -0
- package/dist/insights/provider.mjs +314 -0
- package/dist/insights/queryInput.cjs +93 -0
- package/dist/insights/queryInput.mjs +91 -0
- package/dist/insights/routes.cjs +58 -0
- package/dist/insights/routes.mjs +56 -0
- package/dist/insights/types.d.cts +32 -0
- package/dist/insights/types.d.mts +32 -0
- package/dist/node.d.cts +0 -1
- package/dist/node.d.mts +0 -1
- package/dist/package.cjs +1 -1
- package/dist/package.mjs +1 -1
- package/dist/schema/index.cjs +17 -3
- package/dist/schema/index.mjs +4 -4
- package/dist/schema/v1_0_0.cjs +72 -30
- package/dist/schema/v1_0_0.mjs +72 -31
- package/dist/storageAccess.cjs +0 -1
- package/dist/types/index.d.cts +0 -1
- package/dist/types/index.d.mts +0 -1
- package/package.json +7 -7
- package/src/adapters/databasePluginTestDatabase.ts +14 -0
- package/src/adapters/drizzle.spec.ts +99 -0
- package/src/adapters/drizzle.ts +22 -4
- package/src/adapters/drizzleCrud.ts +154 -2
- package/src/adapters/drizzleLazyDB.ts +17 -3
- package/src/adapters/insightsTransaction.ts +32 -0
- package/src/adapters/kysely.spec.ts +29 -0
- package/src/adapters/kysely.ts +10 -1
- package/src/adapters/kyselyCrud.ts +121 -0
- package/src/adapters/mongodb.integration.spec.ts +210 -0
- package/src/adapters/mongodb.spec.ts +69 -1
- package/src/adapters/mongodb.ts +66 -2
- package/src/adapters/mongodbCollections.ts +5 -0
- package/src/adapters/mongodbQuery.ts +32 -3
- package/src/adapters/mongodbReads.ts +69 -0
- package/src/adapters/mongodbTestClient.ts +41 -6
- package/src/adapters/mongodbTestFilter.ts +4 -1
- package/src/adapters/mongodbWrites.ts +27 -0
- package/src/adapters/prisma.spec.ts +97 -0
- package/src/adapters/prisma.ts +103 -3
- package/src/adapters/prismaRows.ts +37 -1
- package/src/adapters/prismaTestClient.ts +22 -5
- package/src/apiKeys.integration.spec.ts +3 -3
- package/src/createHotUpdaterCore.ts +30 -16
- package/src/db/databasePluginUpdateCheck.bench.ts +3 -0
- package/src/db/fixedMigrator.spec.ts +65 -0
- package/src/db/fixedMigratorMongo.spec.ts +56 -9
- package/src/db/fixedMigratorMongo.ts +10 -10
- package/src/db/index.spec.ts +8 -1
- package/src/db/schema/insightsCollation.ts +21 -0
- package/src/db/schema/sql.ts +8 -5
- package/src/db/schemaGenerators.ts +41 -13
- package/src/db/types.spec.ts +11 -3
- package/src/db/types.ts +13 -7
- package/src/handler-standalone.integration.spec.ts +6 -8
- package/src/handler.ts +12 -12
- package/src/index.ts +3 -3
- package/src/insights/domain.ts +103 -0
- package/src/insights/errors.ts +11 -0
- package/src/{analytics → insights}/eventInput.ts +71 -14
- package/src/insights/insights.integration.spec.ts +381 -0
- package/src/insights/provider.spec.ts +385 -0
- package/src/insights/provider.ts +591 -0
- package/src/insights/queryInput.ts +141 -0
- package/src/insights/routes.ts +104 -0
- package/src/insights/types.ts +55 -0
- package/src/schema/dsl-all-versions.spec.ts +3 -1
- package/src/schema/v1_0_0.spec.ts +77 -3
- package/src/schema/v1_0_0.ts +94 -30
- package/dist/analytics/bounded/activeOverview.cjs +0 -87
- package/dist/analytics/bounded/activeOverview.mjs +0 -85
- package/dist/analytics/bounded/installationSearch.cjs +0 -42
- package/dist/analytics/bounded/installationSearch.mjs +0 -42
- package/dist/analytics/bounded/persistence.cjs +0 -39
- package/dist/analytics/bounded/persistence.mjs +0 -38
- package/dist/analytics/bounded/provider.cjs +0 -170
- package/dist/analytics/bounded/provider.d.cts +0 -7
- package/dist/analytics/bounded/provider.d.mts +0 -7
- package/dist/analytics/bounded/provider.mjs +0 -170
- package/dist/analytics/bounded/scan.cjs +0 -144
- package/dist/analytics/bounded/scan.mjs +0 -138
- package/dist/analytics/domain.d.cts +0 -121
- package/dist/analytics/domain.d.mts +0 -121
- package/dist/analytics/errors.cjs +0 -22
- package/dist/analytics/errors.mjs +0 -20
- package/dist/analytics/persistence.d.cts +0 -6
- package/dist/analytics/persistence.d.mts +0 -6
- package/dist/analytics/queryInput.cjs +0 -58
- package/dist/analytics/queryInput.mjs +0 -55
- package/dist/analytics/routes.cjs +0 -63
- package/dist/analytics/routes.mjs +0 -61
- package/dist/analytics/types.d.cts +0 -17
- package/dist/analytics/types.d.mts +0 -17
- package/src/analytics/analytics.integration.spec.ts +0 -144
- package/src/analytics/bounded/activeOverview.spec.ts +0 -189
- package/src/analytics/bounded/activeOverview.ts +0 -148
- package/src/analytics/bounded/installationSearch.spec.ts +0 -130
- package/src/analytics/bounded/installationSearch.ts +0 -69
- package/src/analytics/bounded/persistence.ts +0 -43
- package/src/analytics/bounded/provider.spec.ts +0 -219
- package/src/analytics/bounded/provider.ts +0 -229
- package/src/analytics/bounded/scan.ts +0 -235
- package/src/analytics/domain.ts +0 -139
- package/src/analytics/errors.ts +0 -18
- package/src/analytics/persistence.ts +0 -10
- package/src/analytics/queryInput.ts +0 -102
- package/src/analytics/routes.ts +0 -123
- package/src/analytics/types.ts +0 -44
package/dist/schema/v1_0_0.mjs
CHANGED
|
@@ -255,19 +255,30 @@ const releaseCatalogsV100 = table("release_catalogs", {
|
|
|
255
255
|
referencedColumns: ["id"]
|
|
256
256
|
})]
|
|
257
257
|
});
|
|
258
|
+
const insightsTextCollations = {
|
|
259
|
+
postgresql: "\"C\"",
|
|
260
|
+
mysql: "utf8mb4_0900_bin"
|
|
261
|
+
};
|
|
262
|
+
const insightsProviders = [
|
|
263
|
+
"postgresql",
|
|
264
|
+
"mysql",
|
|
265
|
+
"sqlite",
|
|
266
|
+
"cockroachdb",
|
|
267
|
+
"mongodb"
|
|
268
|
+
];
|
|
258
269
|
const bundleEventsV100 = table("bundle_events", {
|
|
259
270
|
id: idColumn("id", "uuid"),
|
|
260
|
-
type:
|
|
261
|
-
install_id:
|
|
262
|
-
user_id:
|
|
271
|
+
type: column("type", varchar(32)),
|
|
272
|
+
install_id: column("install_id", varchar(255)).collate(insightsTextCollations),
|
|
273
|
+
user_id: column("user_id", varchar(255)).collate(insightsTextCollations).nullable(),
|
|
263
274
|
username: stringColumn("username").nullable(),
|
|
264
275
|
from_release_id: uuid("from_release_id").nullable(),
|
|
265
276
|
from_bundle_id: uuid("from_bundle_id").nullable(),
|
|
266
277
|
to_release_id: uuid("to_release_id").nullable(),
|
|
267
278
|
to_bundle_id: uuid("to_bundle_id"),
|
|
268
|
-
platform: stringColumn("platform"),
|
|
279
|
+
platform: stringColumn("platform").collate(insightsTextCollations),
|
|
269
280
|
app_version: stringColumn("app_version"),
|
|
270
|
-
channel: stringColumn("channel"),
|
|
281
|
+
channel: stringColumn("channel").collate(insightsTextCollations),
|
|
271
282
|
cohort: stringColumn("cohort"),
|
|
272
283
|
update_strategy: stringColumn("update_strategy").nullable(),
|
|
273
284
|
fingerprint_hash: stringColumn("fingerprint_hash").nullable(),
|
|
@@ -278,43 +289,26 @@ const bundleEventsV100 = table("bundle_events", {
|
|
|
278
289
|
index("bundle_events_received_at_idx", ["received_at_ms", "id"]),
|
|
279
290
|
index("bundle_events_install_idx", [
|
|
280
291
|
"install_id",
|
|
281
|
-
"received_at_ms",
|
|
282
|
-
"id"
|
|
283
|
-
]),
|
|
284
|
-
index("bundle_events_user_id_idx", [
|
|
285
|
-
"user_id",
|
|
286
|
-
"received_at_ms",
|
|
287
|
-
"id"
|
|
288
|
-
]),
|
|
289
|
-
index("bundle_events_username_idx", [
|
|
290
|
-
"username",
|
|
291
|
-
"received_at_ms",
|
|
292
|
-
"id"
|
|
293
|
-
]),
|
|
294
|
-
index("bundle_events_to_bundle_idx", [
|
|
295
292
|
"type",
|
|
296
|
-
"to_bundle_id",
|
|
297
293
|
"received_at_ms",
|
|
298
294
|
"id"
|
|
299
295
|
]),
|
|
300
296
|
index("bundle_events_from_bundle_idx", [
|
|
301
297
|
"type",
|
|
298
|
+
"platform",
|
|
299
|
+
"channel",
|
|
302
300
|
"from_bundle_id",
|
|
303
301
|
"received_at_ms",
|
|
304
302
|
"id"
|
|
305
|
-
]),
|
|
306
|
-
index("
|
|
307
|
-
"type",
|
|
308
|
-
"to_release_id",
|
|
309
|
-
"received_at_ms",
|
|
310
|
-
"id"
|
|
311
|
-
]),
|
|
312
|
-
index("bundle_events_from_release_idx", [
|
|
303
|
+
], insightsProviders),
|
|
304
|
+
index("bundle_events_to_bundle_idx", [
|
|
313
305
|
"type",
|
|
314
|
-
"
|
|
306
|
+
"platform",
|
|
307
|
+
"channel",
|
|
308
|
+
"to_bundle_id",
|
|
315
309
|
"received_at_ms",
|
|
316
310
|
"id"
|
|
317
|
-
])
|
|
311
|
+
], insightsProviders)
|
|
318
312
|
],
|
|
319
313
|
checks: [
|
|
320
314
|
check({
|
|
@@ -339,6 +333,52 @@ const bundleEventsV100 = table("bundle_events", {
|
|
|
339
333
|
})
|
|
340
334
|
]
|
|
341
335
|
});
|
|
336
|
+
const bundleInstallationsV100 = table("bundle_installations", {
|
|
337
|
+
install_id: idColumn("install_id", varchar(255)).collate(insightsTextCollations),
|
|
338
|
+
id: uuid("id"),
|
|
339
|
+
user_id: column("user_id", varchar(255)).collate(insightsTextCollations).nullable(),
|
|
340
|
+
username: stringColumn("username").nullable(),
|
|
341
|
+
to_bundle_id: uuid("to_bundle_id"),
|
|
342
|
+
type: column("type", varchar(32)),
|
|
343
|
+
platform: stringColumn("platform").collate(insightsTextCollations),
|
|
344
|
+
app_version: stringColumn("app_version"),
|
|
345
|
+
channel: stringColumn("channel").collate(insightsTextCollations),
|
|
346
|
+
cohort: stringColumn("cohort"),
|
|
347
|
+
received_at_ms: float("received_at_ms")
|
|
348
|
+
}, {
|
|
349
|
+
indexes: [
|
|
350
|
+
index("bundle_installations_user_id_idx", ["user_id", "install_id"]),
|
|
351
|
+
index("bundle_installations_received_at_idx", ["received_at_ms"]),
|
|
352
|
+
index("bundle_installations_scope_idx", [
|
|
353
|
+
"platform",
|
|
354
|
+
"channel",
|
|
355
|
+
"received_at_ms"
|
|
356
|
+
], insightsProviders),
|
|
357
|
+
index("bundle_installations_bundle_idx", [
|
|
358
|
+
"platform",
|
|
359
|
+
"channel",
|
|
360
|
+
"to_bundle_id",
|
|
361
|
+
"received_at_ms"
|
|
362
|
+
], insightsProviders)
|
|
363
|
+
],
|
|
364
|
+
checks: [
|
|
365
|
+
check({
|
|
366
|
+
name: "bundle_installations_type_check",
|
|
367
|
+
expression: "type in ('UPDATE_APPLIED', 'RECOVERED', 'RELEASE_ADOPTED', 'UNCHANGED')",
|
|
368
|
+
sqliteInline: true
|
|
369
|
+
}),
|
|
370
|
+
check({
|
|
371
|
+
name: "bundle_installations_platform_check",
|
|
372
|
+
expression: "platform in ('ios', 'android')",
|
|
373
|
+
sqliteInline: true
|
|
374
|
+
}),
|
|
375
|
+
check({
|
|
376
|
+
name: "bundle_installations_received_at_check",
|
|
377
|
+
expression: "received_at_ms >= 0",
|
|
378
|
+
sqliteInline: true
|
|
379
|
+
})
|
|
380
|
+
]
|
|
381
|
+
});
|
|
342
382
|
const v1_0_0 = schema({
|
|
343
383
|
version: "1.0.0",
|
|
344
384
|
settingsTable: HOT_UPDATER_SETTINGS_TABLE,
|
|
@@ -349,9 +389,10 @@ const v1_0_0 = schema({
|
|
|
349
389
|
releasesV100,
|
|
350
390
|
releaseCatalogsV100,
|
|
351
391
|
bundleEventsV100,
|
|
392
|
+
bundleInstallationsV100,
|
|
352
393
|
apiKeysV100,
|
|
353
394
|
createSettingsTable("1.0.0")
|
|
354
395
|
]
|
|
355
396
|
});
|
|
356
397
|
//#endregion
|
|
357
|
-
export { apiKeysV100, bundleEventsV100, bundlePatchesV100, bundlesV100, channelsV100, releaseCatalogsV100, releasesV100, v1_0_0 };
|
|
398
|
+
export { apiKeysV100, bundleEventsV100, bundleInstallationsV100, bundlePatchesV100, bundlesV100, channelsV100, releaseCatalogsV100, releasesV100, v1_0_0 };
|
package/dist/storageAccess.cjs
CHANGED
package/dist/types/index.d.cts
CHANGED
package/dist/types/index.d.mts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { HotUpdaterAPI } from "../createHotUpdaterCore.mjs";
|
|
2
2
|
import { ChannelRow } from "@hot-updater/plugin-core";
|
|
3
3
|
import { Bundle as Bundle$1, Bundle as Bundle$2 } from "@hot-updater/core";
|
|
4
|
-
|
|
5
4
|
//#region src/types/index.d.ts
|
|
6
5
|
interface PaginationInfo {
|
|
7
6
|
total: number;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hot-updater/server",
|
|
3
|
-
"version": "1.0.0-rc.
|
|
3
|
+
"version": "1.0.0-rc.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "React Native OTA solution for self-hosted",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -54,10 +54,10 @@
|
|
|
54
54
|
},
|
|
55
55
|
"dependencies": {
|
|
56
56
|
"rou3": "0.7.9",
|
|
57
|
-
"verkit": "0.
|
|
58
|
-
"@hot-updater/
|
|
59
|
-
"@hot-updater/
|
|
60
|
-
"@hot-updater/
|
|
57
|
+
"verkit": "0.4.0",
|
|
58
|
+
"@hot-updater/core": "1.0.0-rc.0",
|
|
59
|
+
"@hot-updater/plugin-core": "1.0.0-rc.2",
|
|
60
|
+
"@hot-updater/bsdiff": "1.0.0-rc.0"
|
|
61
61
|
},
|
|
62
62
|
"peerDependencies": {
|
|
63
63
|
"@prisma/client": "*",
|
|
@@ -91,8 +91,8 @@
|
|
|
91
91
|
"mongodb": "6.20.0",
|
|
92
92
|
"msw": "^2.7.0",
|
|
93
93
|
"uuidv7": "^1.0.2",
|
|
94
|
-
"@hot-updater/standalone": "1.0.0-rc.
|
|
95
|
-
"@hot-updater/test-utils": "1.0.0-rc.
|
|
94
|
+
"@hot-updater/standalone": "1.0.0-rc.2",
|
|
95
|
+
"@hot-updater/test-utils": "1.0.0-rc.2"
|
|
96
96
|
},
|
|
97
97
|
"scripts": {
|
|
98
98
|
"build": "tsdown",
|
|
@@ -84,6 +84,19 @@ export const DATABASE_PLUGIN_TEST_SCHEMA_SQL = `
|
|
|
84
84
|
sdk_version text,
|
|
85
85
|
received_at_ms integer not null
|
|
86
86
|
);
|
|
87
|
+
create table bundle_installations (
|
|
88
|
+
install_id varchar(255) primary key,
|
|
89
|
+
id text not null,
|
|
90
|
+
user_id text,
|
|
91
|
+
username text,
|
|
92
|
+
to_bundle_id text not null,
|
|
93
|
+
type text not null,
|
|
94
|
+
platform text not null,
|
|
95
|
+
app_version text not null,
|
|
96
|
+
channel text not null,
|
|
97
|
+
cohort text not null,
|
|
98
|
+
received_at_ms integer not null
|
|
99
|
+
);
|
|
87
100
|
create table api_keys (
|
|
88
101
|
id text primary key,
|
|
89
102
|
hash text not null unique,
|
|
@@ -97,6 +110,7 @@ export const DATABASE_PLUGIN_TEST_SCHEMA_SQL = `
|
|
|
97
110
|
|
|
98
111
|
export const DATABASE_PLUGIN_TEST_RESET_SQL = `
|
|
99
112
|
delete from bundle_events;
|
|
113
|
+
delete from bundle_installations;
|
|
100
114
|
delete from api_keys;
|
|
101
115
|
delete from bundle_patches;
|
|
102
116
|
delete from release_catalogs;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { PGlite } from "@electric-sql/pglite";
|
|
2
|
+
import { toInsightsInstallationRow } from "@hot-updater/plugin-core";
|
|
2
3
|
import {
|
|
3
4
|
boolean,
|
|
4
5
|
doublePrecision,
|
|
@@ -11,6 +12,7 @@ import {
|
|
|
11
12
|
import { drizzle } from "drizzle-orm/pglite";
|
|
12
13
|
import { describe, expect, it, vi } from "vitest";
|
|
13
14
|
|
|
15
|
+
import { createBundleEventRowFixture } from "../../../test-utils/src/databaseTestFixtures";
|
|
14
16
|
import { setupDatabasePluginTestSuite } from "../../../test-utils/src/setupDatabasePluginTestSuite";
|
|
15
17
|
import type { DatabaseAdapterWithCapabilities } from "../db/types";
|
|
16
18
|
import {
|
|
@@ -100,6 +102,19 @@ const bundleEvents = pgTable("bundle_events", {
|
|
|
100
102
|
sdk_version: text("sdk_version"),
|
|
101
103
|
received_at_ms: integer("received_at_ms").notNull(),
|
|
102
104
|
});
|
|
105
|
+
const bundleInstallations = pgTable("bundle_installations", {
|
|
106
|
+
install_id: varchar("install_id", { length: 255 }).primaryKey(),
|
|
107
|
+
id: text("id").notNull(),
|
|
108
|
+
user_id: text("user_id"),
|
|
109
|
+
username: text("username"),
|
|
110
|
+
to_bundle_id: text("to_bundle_id").notNull(),
|
|
111
|
+
type: text("type").notNull(),
|
|
112
|
+
platform: text("platform").notNull(),
|
|
113
|
+
app_version: text("app_version").notNull(),
|
|
114
|
+
channel: text("channel").notNull(),
|
|
115
|
+
cohort: text("cohort").notNull(),
|
|
116
|
+
received_at_ms: integer("received_at_ms").notNull(),
|
|
117
|
+
});
|
|
103
118
|
const apiKeys = pgTable("api_keys", {
|
|
104
119
|
id: text("id").primaryKey(),
|
|
105
120
|
hash: text("hash").notNull().unique(),
|
|
@@ -111,6 +126,7 @@ const apiKeys = pgTable("api_keys", {
|
|
|
111
126
|
});
|
|
112
127
|
const schema = {
|
|
113
128
|
bundle_events: bundleEvents,
|
|
129
|
+
bundle_installations: bundleInstallations,
|
|
114
130
|
bundle_patches: bundlePatches,
|
|
115
131
|
bundles,
|
|
116
132
|
channels,
|
|
@@ -156,6 +172,87 @@ setupDatabasePluginTestSuite({
|
|
|
156
172
|
});
|
|
157
173
|
|
|
158
174
|
describe("drizzleAdapter schema requirements", () => {
|
|
175
|
+
it("keeps Bun SQLite event and snapshot writes inside its synchronous transaction", async () => {
|
|
176
|
+
const { stdout } = await promisify(execFile)(
|
|
177
|
+
new URL("../../../../node_modules/.bin/bun", import.meta.url).pathname,
|
|
178
|
+
[
|
|
179
|
+
"-e",
|
|
180
|
+
`
|
|
181
|
+
import { Database } from "bun:sqlite";
|
|
182
|
+
import { drizzle } from "drizzle-orm/bun-sqlite";
|
|
183
|
+
import { toInsightsInstallationRow } from "@hot-updater/plugin-core";
|
|
184
|
+
import { drizzleAdapter } from "./src/adapters/drizzle.ts";
|
|
185
|
+
import { createTableSql } from "./src/db/schema/sql.ts";
|
|
186
|
+
import { createBundleEventRowFixture } from "../test-utils/src/databaseTestFixtures.ts";
|
|
187
|
+
import * as schema from "../../examples-server/elysia-drizzle-libsql/hot-updater-schema.ts";
|
|
188
|
+
const db = new Database(":memory:");
|
|
189
|
+
db.exec(createTableSql("sqlite").join(";"));
|
|
190
|
+
db.exec("create trigger reject_snapshot before insert on bundle_installations begin select raise(abort, 'injected snapshot failure'); end;");
|
|
191
|
+
const plugin = drizzleAdapter({ db: drizzle(db, { schema }), provider: "sqlite" });
|
|
192
|
+
const event = createBundleEventRowFixture("708", 100);
|
|
193
|
+
const input = { event, installation: toInsightsInstallationRow(event) };
|
|
194
|
+
let rejected = false;
|
|
195
|
+
try { await plugin.models.insights.record(input); } catch { rejected = true; }
|
|
196
|
+
if (!rejected || db.query("select count(*) as total from bundle_events").get().total !== 0) throw new Error("transaction failed to roll back");
|
|
197
|
+
db.exec("drop trigger reject_snapshot");
|
|
198
|
+
await plugin.models.insights.record(input);
|
|
199
|
+
await plugin.models.insights.record(input);
|
|
200
|
+
if (db.query("select count(*) as total from bundle_events").get().total !== 1 || db.query("select count(*) as total from bundle_installations").get().total !== 1) throw new Error("retry did not commit exactly one report");
|
|
201
|
+
db.close();
|
|
202
|
+
console.log("atomic retry verified");
|
|
203
|
+
`,
|
|
204
|
+
],
|
|
205
|
+
{ cwd: new URL("../..", import.meta.url).pathname },
|
|
206
|
+
);
|
|
207
|
+
expect(stdout).toContain("atomic retry verified");
|
|
208
|
+
});
|
|
209
|
+
it("rolls back the event when the installation write fails and permits retry", async () => {
|
|
210
|
+
const db = new PGlite();
|
|
211
|
+
await db.exec(DATABASE_PLUGIN_TEST_SCHEMA_SQL);
|
|
212
|
+
await db.exec(
|
|
213
|
+
"alter table bundle_installations add constraint reject_snapshot check (install_id <> 'install-701')",
|
|
214
|
+
);
|
|
215
|
+
const plugin = drizzleAdapter({
|
|
216
|
+
db: drizzle(db, { schema }),
|
|
217
|
+
provider: "postgresql",
|
|
218
|
+
});
|
|
219
|
+
const event = createBundleEventRowFixture("701", 100);
|
|
220
|
+
const input = { event, installation: toInsightsInstallationRow(event) };
|
|
221
|
+
try {
|
|
222
|
+
await expect(plugin.models.insights.record(input)).rejects.toThrow();
|
|
223
|
+
expect((await db.query("select id from bundle_events")).rows).toEqual([]);
|
|
224
|
+
await db.exec(
|
|
225
|
+
"alter table bundle_installations drop constraint reject_snapshot",
|
|
226
|
+
);
|
|
227
|
+
await plugin.models.insights.record(input);
|
|
228
|
+
await expect(
|
|
229
|
+
plugin.models.insights.findInstallations({
|
|
230
|
+
installId: event.install_id,
|
|
231
|
+
}),
|
|
232
|
+
).resolves.toEqual([input.installation]);
|
|
233
|
+
} finally {
|
|
234
|
+
await db.close();
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it("requires transaction support before recording on a lazy database", async () => {
|
|
239
|
+
const getDB = vi.fn(() => {
|
|
240
|
+
throw new DrizzleTestStateError();
|
|
241
|
+
});
|
|
242
|
+
const plugin = drizzleAdapter({
|
|
243
|
+
db: getDB,
|
|
244
|
+
provider: "postgresql",
|
|
245
|
+
schema,
|
|
246
|
+
});
|
|
247
|
+
const event = createBundleEventRowFixture("702", 100);
|
|
248
|
+
await expect(
|
|
249
|
+
plugin.models.insights.record({
|
|
250
|
+
event,
|
|
251
|
+
installation: toInsightsInstallationRow(event),
|
|
252
|
+
}),
|
|
253
|
+
).rejects.toThrow("transaction support");
|
|
254
|
+
expect(getDB).not.toHaveBeenCalled();
|
|
255
|
+
});
|
|
159
256
|
it("does not resolve a lazy database while generating a schema", () => {
|
|
160
257
|
const getDB = vi.fn(() => {
|
|
161
258
|
throw new DrizzleTestStateError();
|
|
@@ -235,3 +332,5 @@ describe("drizzleAdapter schema requirements", () => {
|
|
|
235
332
|
);
|
|
236
333
|
});
|
|
237
334
|
});
|
|
335
|
+
import { execFile } from "node:child_process";
|
|
336
|
+
import { promisify } from "node:util";
|
package/src/adapters/drizzle.ts
CHANGED
|
@@ -15,8 +15,9 @@ import type {
|
|
|
15
15
|
ORMProvider,
|
|
16
16
|
SchemaGenerator,
|
|
17
17
|
} from "../db/types";
|
|
18
|
-
import { createDrizzleCrud } from "./drizzleCrud";
|
|
18
|
+
import { createDrizzleCrud, recordDrizzleInsights } from "./drizzleCrud";
|
|
19
19
|
import { createLazyDB } from "./drizzleLazyDB";
|
|
20
|
+
import { runInsightsTransaction } from "./insightsTransaction";
|
|
20
21
|
|
|
21
22
|
export type DrizzleProvider = Exclude<
|
|
22
23
|
ORMProvider,
|
|
@@ -38,6 +39,18 @@ const createImplementation = (
|
|
|
38
39
|
const transaction = db.transaction?.bind(db);
|
|
39
40
|
return {
|
|
40
41
|
...crud,
|
|
42
|
+
recordInsights: (input) => {
|
|
43
|
+
if (transaction === undefined) {
|
|
44
|
+
throw new Error(
|
|
45
|
+
"Drizzle Insights recording requires transaction support.",
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
return runInsightsTransaction(() =>
|
|
49
|
+
transaction((transactionDatabase) =>
|
|
50
|
+
recordDrizzleInsights(transactionDatabase, config.provider, input),
|
|
51
|
+
),
|
|
52
|
+
);
|
|
53
|
+
},
|
|
41
54
|
deleteChannel: (input) => {
|
|
42
55
|
if (transaction === undefined) {
|
|
43
56
|
throw new Error(
|
|
@@ -115,9 +128,14 @@ export const drizzleAdapter = (
|
|
|
115
128
|
list: (input) => getAdapter().models.channels.list(input),
|
|
116
129
|
delete: (input) => getAdapter().models.channels.delete(input),
|
|
117
130
|
},
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
131
|
+
insights: {
|
|
132
|
+
record: (input) => getAdapter().models.insights.record(input),
|
|
133
|
+
listEvents: (input) => getAdapter().models.insights.listEvents(input),
|
|
134
|
+
findInstallations: (input) =>
|
|
135
|
+
getAdapter().models.insights.findInstallations(input),
|
|
136
|
+
countInstallations: (input) =>
|
|
137
|
+
getAdapter().models.insights.countInstallations(input),
|
|
138
|
+
countEvents: (input) => getAdapter().models.insights.countEvents(input),
|
|
121
139
|
},
|
|
122
140
|
apiKeys: {
|
|
123
141
|
create: (row) => getAdapter().models.apiKeys.create(row),
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
DatabasePluginInputError,
|
|
3
|
+
type InsightsRecordInput,
|
|
4
|
+
} from "@hot-updater/plugin-core";
|
|
2
5
|
import type {
|
|
3
6
|
DatabasePluginImplementation,
|
|
4
7
|
TransactionDatabasePluginImplementation,
|
|
5
8
|
} from "@hot-updater/plugin-core/internal";
|
|
6
|
-
import { asc, desc, eq, sql, type SQLWrapper } from "drizzle-orm";
|
|
9
|
+
import { and, asc, desc, eq, lt, or, sql, type SQLWrapper } from "drizzle-orm";
|
|
7
10
|
|
|
8
11
|
import {
|
|
9
12
|
isChannelDeleteReferencedError,
|
|
@@ -78,9 +81,98 @@ const executeInsert = async (
|
|
|
78
81
|
await ignored.execute();
|
|
79
82
|
};
|
|
80
83
|
|
|
84
|
+
export const recordDrizzleInsights = (
|
|
85
|
+
db: DrizzleDB,
|
|
86
|
+
provider: DrizzleProvider,
|
|
87
|
+
input: InsightsRecordInput,
|
|
88
|
+
): void | Promise<void> => {
|
|
89
|
+
if (db.resultKind !== "sync")
|
|
90
|
+
return recordAsyncDrizzleInsights(db, provider, input);
|
|
91
|
+
const events = getDrizzleTable(db, "bundle_events");
|
|
92
|
+
const installations = getDrizzleTable(db, "bundle_installations");
|
|
93
|
+
const eventInsert = db
|
|
94
|
+
.insert(events)
|
|
95
|
+
.values(input.event)
|
|
96
|
+
.onConflictDoNothing?.()
|
|
97
|
+
.returning?.({ id: getDrizzleColumn(events, "id") });
|
|
98
|
+
const installationInsert = db
|
|
99
|
+
.insert(installations)
|
|
100
|
+
.values(input.installation)
|
|
101
|
+
.onConflictDoNothing?.();
|
|
102
|
+
const installationUpdate = db
|
|
103
|
+
.update(installations)
|
|
104
|
+
.set(input.installation)
|
|
105
|
+
.where(newerInstallationWhere(installations, input.installation));
|
|
106
|
+
if (
|
|
107
|
+
eventInsert?.all === undefined ||
|
|
108
|
+
installationInsert?.run === undefined ||
|
|
109
|
+
installationUpdate.run === undefined
|
|
110
|
+
)
|
|
111
|
+
throw new DrizzleAdapterInvariantError();
|
|
112
|
+
if (eventInsert.all().length === 0) return;
|
|
113
|
+
installationInsert.run();
|
|
114
|
+
installationUpdate.run();
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const newerInstallationWhere = (
|
|
118
|
+
installations: DrizzleTable,
|
|
119
|
+
installation: InsightsRecordInput["installation"],
|
|
120
|
+
) => {
|
|
121
|
+
const receivedAt = getDrizzleColumn(installations, "received_at_ms");
|
|
122
|
+
return and(
|
|
123
|
+
eq(getDrizzleColumn(installations, "install_id"), installation.install_id),
|
|
124
|
+
or(
|
|
125
|
+
lt(receivedAt, installation.received_at_ms),
|
|
126
|
+
and(
|
|
127
|
+
eq(receivedAt, installation.received_at_ms),
|
|
128
|
+
lt(getDrizzleColumn(installations, "id"), installation.id),
|
|
129
|
+
),
|
|
130
|
+
),
|
|
131
|
+
);
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const recordAsyncDrizzleInsights = async (
|
|
135
|
+
db: DrizzleDB,
|
|
136
|
+
provider: DrizzleProvider,
|
|
137
|
+
{ event, installation }: InsightsRecordInput,
|
|
138
|
+
): Promise<void> => {
|
|
139
|
+
const events = getDrizzleTable(db, "bundle_events");
|
|
140
|
+
const installations = getDrizzleTable(db, "bundle_installations");
|
|
141
|
+
if (provider === "mysql") {
|
|
142
|
+
const existing = await db.query.bundle_events.findFirst({
|
|
143
|
+
where: eq(getDrizzleColumn(events, "id"), event.id),
|
|
144
|
+
});
|
|
145
|
+
if (existing !== undefined) return;
|
|
146
|
+
await db.insert(events).values(event).execute();
|
|
147
|
+
const insertInstallation = db
|
|
148
|
+
.insert(installations)
|
|
149
|
+
.values(installation)
|
|
150
|
+
.onDuplicateKeyUpdate?.({ set: { install_id: installation.install_id } });
|
|
151
|
+
if (insertInstallation === undefined)
|
|
152
|
+
throw new DrizzleAdapterInvariantError();
|
|
153
|
+
await insertInstallation.execute();
|
|
154
|
+
} else {
|
|
155
|
+
const insert = db.insert(events).values(event).onConflictDoNothing?.();
|
|
156
|
+
const returning = insert?.returning?.({
|
|
157
|
+
id: getDrizzleColumn(events, "id"),
|
|
158
|
+
});
|
|
159
|
+
if (returning === undefined) throw new DrizzleAdapterInvariantError();
|
|
160
|
+
const rows = await returning.execute();
|
|
161
|
+
if (!Array.isArray(rows)) throw new DrizzleAdapterInvariantError();
|
|
162
|
+
if (rows.length === 0) return;
|
|
163
|
+
await executeInsert(db, provider, installations, installation, "ignore");
|
|
164
|
+
}
|
|
165
|
+
await db
|
|
166
|
+
.update(installations)
|
|
167
|
+
.set(installation)
|
|
168
|
+
.where(newerInstallationWhere(installations, installation))
|
|
169
|
+
.execute();
|
|
170
|
+
};
|
|
171
|
+
|
|
81
172
|
const toOrderBy = (
|
|
82
173
|
table: DrizzleTable,
|
|
83
174
|
input: {
|
|
175
|
+
model?: string;
|
|
84
176
|
orderBy?: readonly {
|
|
85
177
|
field: string;
|
|
86
178
|
direction: "asc" | "desc";
|
|
@@ -91,6 +183,12 @@ const toOrderBy = (
|
|
|
91
183
|
const clauses = input.orderBy;
|
|
92
184
|
return clauses?.flatMap((clause) => {
|
|
93
185
|
const column = getDrizzleColumn(table, clause.field);
|
|
186
|
+
if (
|
|
187
|
+
input.model === "bundle_events" ||
|
|
188
|
+
input.model === "bundle_installations"
|
|
189
|
+
) {
|
|
190
|
+
return [clause.direction === "asc" ? asc(column) : desc(column)];
|
|
191
|
+
}
|
|
94
192
|
const nulls =
|
|
95
193
|
clause.nulls ?? (clause.direction === "asc" ? "last" : "first");
|
|
96
194
|
return [
|
|
@@ -110,6 +208,7 @@ export const createDrizzleCrud = (
|
|
|
110
208
|
const bundles = getDrizzleTable(db, "bundles");
|
|
111
209
|
const patches = getDrizzleTable(db, "bundle_patches");
|
|
112
210
|
const events = getDrizzleTable(db, "bundle_events");
|
|
211
|
+
const installations = getDrizzleTable(db, "bundle_installations");
|
|
113
212
|
const releases = getDrizzleTable(db, "releases");
|
|
114
213
|
const releaseCatalogs = getDrizzleTable(db, "release_catalogs");
|
|
115
214
|
const channels = getDrizzleTable(db, "channels");
|
|
@@ -169,6 +268,15 @@ export const createDrizzleCrud = (
|
|
|
169
268
|
case "bundle_events":
|
|
170
269
|
await executeInsert(db, provider, events, input.data, undefined);
|
|
171
270
|
return input.data;
|
|
271
|
+
case "bundle_installations":
|
|
272
|
+
await executeInsert(
|
|
273
|
+
db,
|
|
274
|
+
provider,
|
|
275
|
+
installations,
|
|
276
|
+
input.data,
|
|
277
|
+
input.onConflict,
|
|
278
|
+
);
|
|
279
|
+
return input.data;
|
|
172
280
|
case "releases":
|
|
173
281
|
await executeInsert(
|
|
174
282
|
db,
|
|
@@ -208,6 +316,27 @@ export const createDrizzleCrud = (
|
|
|
208
316
|
}
|
|
209
317
|
},
|
|
210
318
|
async update(input) {
|
|
319
|
+
if (input.model === "bundle_installations") {
|
|
320
|
+
const installId = input.where.find(
|
|
321
|
+
(item) =>
|
|
322
|
+
item.field === "install_id" &&
|
|
323
|
+
(item.operator === undefined || item.operator === "eq") &&
|
|
324
|
+
typeof item.value === "string",
|
|
325
|
+
)?.value;
|
|
326
|
+
if (typeof installId !== "string") {
|
|
327
|
+
throw new DrizzleAdapterInvariantError();
|
|
328
|
+
}
|
|
329
|
+
const where = buildDrizzleWhere(provider, installations, input.where);
|
|
330
|
+
if (where === undefined) throw new DrizzleAdapterInvariantError();
|
|
331
|
+
await db.update(installations).set(input.update).where(where).execute();
|
|
332
|
+
const row = await db.query.bundle_installations.findFirst({
|
|
333
|
+
where: eq(getDrizzleColumn(installations, "install_id"), installId),
|
|
334
|
+
});
|
|
335
|
+
return row?.id === input.update.id &&
|
|
336
|
+
row.received_at_ms === input.update.received_at_ms
|
|
337
|
+
? row
|
|
338
|
+
: null;
|
|
339
|
+
}
|
|
211
340
|
const selector = input.where[0];
|
|
212
341
|
if (selector === undefined || typeof selector.value !== "string") {
|
|
213
342
|
throw new DrizzleAdapterInvariantError();
|
|
@@ -296,6 +425,11 @@ export const createDrizzleCrud = (
|
|
|
296
425
|
throw new DatabasePluginInputError("invalid-operation");
|
|
297
426
|
}
|
|
298
427
|
switch (input.model) {
|
|
428
|
+
case "bundle_events":
|
|
429
|
+
return db.$count(
|
|
430
|
+
events,
|
|
431
|
+
buildDrizzleWhere(provider, events, input.where),
|
|
432
|
+
);
|
|
299
433
|
case "bundles":
|
|
300
434
|
return db.$count(
|
|
301
435
|
bundles,
|
|
@@ -311,6 +445,11 @@ export const createDrizzleCrud = (
|
|
|
311
445
|
releases,
|
|
312
446
|
buildDrizzleWhere(provider, releases, input.where),
|
|
313
447
|
);
|
|
448
|
+
case "bundle_installations":
|
|
449
|
+
return db.$count(
|
|
450
|
+
installations,
|
|
451
|
+
buildDrizzleWhere(provider, installations, input.where),
|
|
452
|
+
);
|
|
314
453
|
}
|
|
315
454
|
},
|
|
316
455
|
async findOne(input) {
|
|
@@ -351,6 +490,12 @@ export const createDrizzleCrud = (
|
|
|
351
490
|
});
|
|
352
491
|
return row === undefined ? null : fromStoredReleaseCatalogRow(row);
|
|
353
492
|
}
|
|
493
|
+
case "bundle_installations":
|
|
494
|
+
return (
|
|
495
|
+
(await db.query.bundle_installations.findFirst({
|
|
496
|
+
where: buildDrizzleWhere(provider, installations, input.where),
|
|
497
|
+
})) ?? null
|
|
498
|
+
);
|
|
354
499
|
}
|
|
355
500
|
},
|
|
356
501
|
async findMany(input) {
|
|
@@ -374,6 +519,13 @@ export const createDrizzleCrud = (
|
|
|
374
519
|
limit: input.limit,
|
|
375
520
|
offset: input.offset,
|
|
376
521
|
});
|
|
522
|
+
case "bundle_installations":
|
|
523
|
+
return db.query.bundle_installations.findMany({
|
|
524
|
+
where: buildDrizzleWhere(provider, installations, input.where),
|
|
525
|
+
orderBy: toOrderBy(installations, input),
|
|
526
|
+
limit: input.limit,
|
|
527
|
+
offset: input.offset,
|
|
528
|
+
});
|
|
377
529
|
case "api_keys":
|
|
378
530
|
return db.query.api_keys.findMany({
|
|
379
531
|
where: buildDrizzleWhere(provider, apiKeys, input.where),
|