@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
|
@@ -2,6 +2,7 @@ import { MongoClient } from "mongodb";
|
|
|
2
2
|
import { describe, expect, it, vi } from "vitest";
|
|
3
3
|
|
|
4
4
|
import { createInMemoryDatabasePlugin } from "../../../test-utils/test/inMemoryDatabasePlugin";
|
|
5
|
+
import { HOT_UPDATER_SCHEMA_VERSION } from "../schema/types";
|
|
5
6
|
import { createDatabasePluginCore } from "./databasePluginCore";
|
|
6
7
|
import { createMongoMigrator } from "./fixedMigrator";
|
|
7
8
|
import { createSchemaReadinessChecker } from "./schemaReadiness";
|
|
@@ -29,7 +30,7 @@ function createSettingsMongoClient(
|
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
describe("MongoDB migration", () => {
|
|
32
|
-
it("creates schema
|
|
33
|
+
it("creates the current schema from an empty database", async () => {
|
|
33
34
|
const settings = new Map<string, unknown>();
|
|
34
35
|
const settingsCollection = {
|
|
35
36
|
find: ({ key }: { readonly key: string }) => ({
|
|
@@ -50,7 +51,7 @@ describe("MongoDB migration", () => {
|
|
|
50
51
|
const modelCollection = {
|
|
51
52
|
find: () => ({ toArray: async () => [] }),
|
|
52
53
|
listIndexes: () => ({ toArray: async () => [] }),
|
|
53
|
-
createIndex: async () => "created",
|
|
54
|
+
createIndex: vi.fn(async () => "created"),
|
|
54
55
|
};
|
|
55
56
|
const command = vi.fn(
|
|
56
57
|
async (_input: Record<string, unknown>): Promise<void> => undefined,
|
|
@@ -76,7 +77,47 @@ describe("MongoDB migration", () => {
|
|
|
76
77
|
{ key: 1 },
|
|
77
78
|
{ unique: true },
|
|
78
79
|
);
|
|
79
|
-
expect(settings.get("schema.core")).toBe(
|
|
80
|
+
expect(settings.get("schema.core")).toBe(HOT_UPDATER_SCHEMA_VERSION);
|
|
81
|
+
expect(modelCollection.createIndex).toHaveBeenCalledWith(
|
|
82
|
+
{ install_id: 1 },
|
|
83
|
+
{
|
|
84
|
+
name: "bundle_installations_install_id_idx",
|
|
85
|
+
unique: true,
|
|
86
|
+
collation: { locale: "simple" },
|
|
87
|
+
},
|
|
88
|
+
);
|
|
89
|
+
expect(modelCollection.createIndex).toHaveBeenCalledWith(
|
|
90
|
+
{
|
|
91
|
+
type: 1,
|
|
92
|
+
platform: 1,
|
|
93
|
+
channel: 1,
|
|
94
|
+
from_bundle_id: 1,
|
|
95
|
+
received_at_ms: 1,
|
|
96
|
+
id: 1,
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
name: "bundle_events_from_bundle_idx",
|
|
100
|
+
collation: { locale: "simple" },
|
|
101
|
+
},
|
|
102
|
+
);
|
|
103
|
+
expect(modelCollection.createIndex).toHaveBeenCalledWith(
|
|
104
|
+
{
|
|
105
|
+
type: 1,
|
|
106
|
+
platform: 1,
|
|
107
|
+
channel: 1,
|
|
108
|
+
to_bundle_id: 1,
|
|
109
|
+
received_at_ms: 1,
|
|
110
|
+
id: 1,
|
|
111
|
+
},
|
|
112
|
+
{ name: "bundle_events_to_bundle_idx", collation: { locale: "simple" } },
|
|
113
|
+
);
|
|
114
|
+
expect(modelCollection.createIndex).toHaveBeenCalledWith(
|
|
115
|
+
{ platform: 1, channel: 1, to_bundle_id: 1, received_at_ms: 1 },
|
|
116
|
+
{
|
|
117
|
+
name: "bundle_installations_bundle_idx",
|
|
118
|
+
collation: { locale: "simple" },
|
|
119
|
+
},
|
|
120
|
+
);
|
|
80
121
|
const commands = command.mock.calls.map(([input]) => input);
|
|
81
122
|
expect(commands.find(({ collMod }) => collMod === "bundles")).toMatchObject(
|
|
82
123
|
{
|
|
@@ -138,24 +179,30 @@ describe("MongoDB migration", () => {
|
|
|
138
179
|
});
|
|
139
180
|
});
|
|
140
181
|
|
|
141
|
-
it("is a no-op when schema.core is
|
|
142
|
-
const client = createSettingsMongoClient({
|
|
182
|
+
it("is a no-op when schema.core is current", async () => {
|
|
183
|
+
const client = createSettingsMongoClient({
|
|
184
|
+
"schema.core": HOT_UPDATER_SCHEMA_VERSION,
|
|
185
|
+
});
|
|
143
186
|
const migrator = createMongoMigrator(client);
|
|
144
187
|
|
|
145
|
-
await expect(migrator.getVersion()).resolves.toBe(
|
|
188
|
+
await expect(migrator.getVersion()).resolves.toBe(
|
|
189
|
+
HOT_UPDATER_SCHEMA_VERSION,
|
|
190
|
+
);
|
|
146
191
|
await expect(
|
|
147
192
|
migrator.migrateToLatest({ mode: "from-schema" }),
|
|
148
193
|
).resolves.toMatchObject({ operations: [] });
|
|
149
194
|
});
|
|
150
195
|
|
|
151
|
-
it("ignores a leftover version marker when schema.core is
|
|
196
|
+
it("ignores a leftover version marker when schema.core is current", async () => {
|
|
152
197
|
const client = createSettingsMongoClient({
|
|
153
|
-
"schema.core":
|
|
198
|
+
"schema.core": HOT_UPDATER_SCHEMA_VERSION,
|
|
154
199
|
version: "0.36.0",
|
|
155
200
|
});
|
|
156
201
|
const migrator = createMongoMigrator(client);
|
|
157
202
|
|
|
158
|
-
await expect(migrator.getVersion()).resolves.toBe(
|
|
203
|
+
await expect(migrator.getVersion()).resolves.toBe(
|
|
204
|
+
HOT_UPDATER_SCHEMA_VERSION,
|
|
205
|
+
);
|
|
159
206
|
await expect(
|
|
160
207
|
migrator.migrateToLatest({ mode: "from-schema" }),
|
|
161
208
|
).resolves.toMatchObject({ operations: [] });
|
|
@@ -334,7 +334,11 @@ export const createMongoMigrator = (client: MongoClient): Migrator => {
|
|
|
334
334
|
await ensureSettingsKeyIndex();
|
|
335
335
|
for (const table of hotUpdaterSchema.tables) {
|
|
336
336
|
if (table.internal) continue;
|
|
337
|
+
const isInsights =
|
|
338
|
+
table.ormName === "bundle_events" ||
|
|
339
|
+
table.ormName === "bundle_installations";
|
|
337
340
|
const collection = db.collection(table.ormName);
|
|
341
|
+
const collation = isInsights ? { locale: "simple" } : undefined;
|
|
338
342
|
const primaryKey = table.columns.find(
|
|
339
343
|
(column) => column.primaryKey,
|
|
340
344
|
);
|
|
@@ -344,18 +348,13 @@ export const createMongoMigrator = (client: MongoClient): Migrator => {
|
|
|
344
348
|
);
|
|
345
349
|
}
|
|
346
350
|
const idIndexName = `${table.ormName}_${primaryKey.ormName}_idx`;
|
|
347
|
-
const existingIndexes = await collection
|
|
348
|
-
.listIndexes()
|
|
349
|
-
.toArray();
|
|
350
|
-
const existingIdIndex = existingIndexes.find(
|
|
351
|
-
({ name }) => name === idIndexName,
|
|
352
|
-
);
|
|
353
|
-
if (existingIdIndex && existingIdIndex.unique !== true) {
|
|
354
|
-
await collection.dropIndex(idIndexName);
|
|
355
|
-
}
|
|
356
351
|
await collection.createIndex(
|
|
357
352
|
{ [primaryKey.ormName]: 1 },
|
|
358
|
-
{
|
|
353
|
+
{
|
|
354
|
+
name: idIndexName,
|
|
355
|
+
unique: true,
|
|
356
|
+
...(collation ? { collation } : {}),
|
|
357
|
+
},
|
|
359
358
|
);
|
|
360
359
|
for (const index of (table.indexes ?? []).filter((item) =>
|
|
361
360
|
schemaIndexAppliesToProvider(item, "mongodb"),
|
|
@@ -367,6 +366,7 @@ export const createMongoMigrator = (client: MongoClient): Migrator => {
|
|
|
367
366
|
{
|
|
368
367
|
name: index.name,
|
|
369
368
|
...(index.unique ? { unique: true } : {}),
|
|
369
|
+
...(collation ? { collation } : {}),
|
|
370
370
|
},
|
|
371
371
|
);
|
|
372
372
|
}
|
package/src/db/index.spec.ts
CHANGED
|
@@ -415,7 +415,7 @@ describe("server/db hotUpdater (PGlite + Kysely)", async () => {
|
|
|
415
415
|
});
|
|
416
416
|
|
|
417
417
|
describe("migrator enhancements", () => {
|
|
418
|
-
it("registers only schema
|
|
418
|
+
it("registers only the initial schema", () => {
|
|
419
419
|
expect(hotUpdaterSchemaVersions.map((schema) => schema.version)).toEqual([
|
|
420
420
|
"1.0.0",
|
|
421
421
|
]);
|
|
@@ -837,6 +837,9 @@ describe("server/db hotUpdater (PGlite + Kysely)", async () => {
|
|
|
837
837
|
bundle_events: {
|
|
838
838
|
id: "event_id",
|
|
839
839
|
},
|
|
840
|
+
bundle_installations: {
|
|
841
|
+
install_id: "install_id",
|
|
842
|
+
},
|
|
840
843
|
bundle_patches: {
|
|
841
844
|
bundle_id: "bundle_id",
|
|
842
845
|
id: "patch_id",
|
|
@@ -878,6 +881,10 @@ describe("server/db hotUpdater (PGlite + Kysely)", async () => {
|
|
|
878
881
|
findFirst: vi.fn(),
|
|
879
882
|
findMany: vi.fn(),
|
|
880
883
|
},
|
|
884
|
+
bundle_installations: {
|
|
885
|
+
findFirst: vi.fn(),
|
|
886
|
+
findMany: vi.fn(),
|
|
887
|
+
},
|
|
881
888
|
bundle_patches: {
|
|
882
889
|
findFirst: vi.fn(),
|
|
883
890
|
findMany: vi.fn(),
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { bundleEventsV100, bundleInstallationsV100 } from "../../schema/v1_0_0";
|
|
2
|
+
import type { ORMSQLProvider } from "../types";
|
|
3
|
+
import { getSqlType, sqlColumnDefinition } from "./sql";
|
|
4
|
+
|
|
5
|
+
export const getInsightsCollationSql = (
|
|
6
|
+
provider: ORMSQLProvider,
|
|
7
|
+
): readonly string[] =>
|
|
8
|
+
[bundleEventsV100, bundleInstallationsV100].flatMap((table) =>
|
|
9
|
+
table.columns.flatMap((column) => {
|
|
10
|
+
const collation = column.providerCollations?.[provider];
|
|
11
|
+
if (collation === undefined) return [];
|
|
12
|
+
if (provider === "mysql") {
|
|
13
|
+
return [
|
|
14
|
+
`alter table ${table.ormName} modify column ${sqlColumnDefinition(table, { ...column, primaryKey: undefined }, provider)}`,
|
|
15
|
+
];
|
|
16
|
+
}
|
|
17
|
+
return [
|
|
18
|
+
`alter table ${table.ormName} alter column ${column.ormName} type ${getSqlType(column.type, provider)} collate ${collation}`,
|
|
19
|
+
];
|
|
20
|
+
}),
|
|
21
|
+
);
|
package/src/db/schema/sql.ts
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
type HotUpdaterColumnType,
|
|
6
6
|
type HotUpdaterForeignKeySchema,
|
|
7
7
|
type HotUpdaterTableSchema,
|
|
8
|
+
type HotUpdaterVersionedSchema,
|
|
8
9
|
} from "../../schema/types";
|
|
9
10
|
import type {
|
|
10
11
|
MigrationOperation,
|
|
@@ -209,9 +210,10 @@ export const createTableStatement = (
|
|
|
209
210
|
export const createForeignKeySqlStatements = (
|
|
210
211
|
provider: ORMSQLProvider,
|
|
211
212
|
relationMode: RelationMode = "foreign-keys",
|
|
213
|
+
schema: HotUpdaterVersionedSchema = hotUpdaterSchema,
|
|
212
214
|
): readonly string[] => {
|
|
213
215
|
if (relationMode !== "foreign-keys" || provider === "sqlite") return [];
|
|
214
|
-
return
|
|
216
|
+
return schema.tables.flatMap((table) =>
|
|
215
217
|
(table.foreignKeys ?? []).map((foreignKey) =>
|
|
216
218
|
createForeignKeySql(table, foreignKey, provider),
|
|
217
219
|
),
|
|
@@ -221,21 +223,22 @@ export const createForeignKeySqlStatements = (
|
|
|
221
223
|
export const createTableSql = (
|
|
222
224
|
provider: ORMSQLProvider,
|
|
223
225
|
relationMode: RelationMode = "foreign-keys",
|
|
226
|
+
schema: HotUpdaterVersionedSchema = hotUpdaterSchema,
|
|
224
227
|
): readonly string[] => [
|
|
225
|
-
...
|
|
228
|
+
...schema.tables.map((table) =>
|
|
226
229
|
createTableStatement(table, provider, relationMode),
|
|
227
230
|
),
|
|
228
|
-
...
|
|
231
|
+
...schema.tables.flatMap((table) =>
|
|
229
232
|
(table.indexes ?? [])
|
|
230
233
|
.filter((index) => schemaIndexAppliesToProvider(index, provider))
|
|
231
234
|
.map((index) => createIndexSql(table, index, provider)),
|
|
232
235
|
),
|
|
233
236
|
...(provider === "sqlite"
|
|
234
237
|
? []
|
|
235
|
-
:
|
|
238
|
+
: schema.tables.flatMap((table) =>
|
|
236
239
|
(table.checks ?? []).map((check) =>
|
|
237
240
|
createCheckSql(table, check, provider),
|
|
238
241
|
),
|
|
239
242
|
)),
|
|
240
|
-
...createForeignKeySqlStatements(provider, relationMode),
|
|
243
|
+
...createForeignKeySqlStatements(provider, relationMode, schema),
|
|
241
244
|
];
|
|
@@ -7,11 +7,13 @@ import {
|
|
|
7
7
|
type HotUpdaterTableSchema,
|
|
8
8
|
type HotUpdaterVersionedSchema,
|
|
9
9
|
} from "../schema/types";
|
|
10
|
+
import { getInsightsCollationSql } from "./schema/insightsCollation";
|
|
10
11
|
import {
|
|
11
12
|
getHotUpdaterSchemaVersion,
|
|
12
13
|
hotUpdaterSchema,
|
|
13
14
|
schemaIndexAppliesToProvider,
|
|
14
15
|
} from "./schema/registry";
|
|
16
|
+
import { getSqlType } from "./schema/sql";
|
|
15
17
|
import type { ORMProvider, ORMSQLProvider, SchemaGenerator } from "./types";
|
|
16
18
|
import { getSQLProvider } from "./types";
|
|
17
19
|
|
|
@@ -154,19 +156,33 @@ const prismaIndexes = (
|
|
|
154
156
|
export const generatePrismaSchema = (
|
|
155
157
|
provider: ORMProvider,
|
|
156
158
|
schema: HotUpdaterVersionedSchema = hotUpdaterSchema,
|
|
157
|
-
) =>
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
.join("\n")
|
|
168
|
-
|
|
169
|
-
|
|
159
|
+
) => {
|
|
160
|
+
const sqlProvider = getSQLProvider(provider);
|
|
161
|
+
const collationSql = sqlProvider ? getInsightsCollationSql(sqlProvider) : [];
|
|
162
|
+
const header =
|
|
163
|
+
collationSql.length === 0
|
|
164
|
+
? ""
|
|
165
|
+
: [
|
|
166
|
+
"// Apply these statements in the initial Prisma SQL migration to preserve exact Insights identities and cursors:",
|
|
167
|
+
...collationSql.map((statement) => `// ${statement};`),
|
|
168
|
+
"",
|
|
169
|
+
].join("\n");
|
|
170
|
+
return (
|
|
171
|
+
header +
|
|
172
|
+
schema.tables
|
|
173
|
+
.map((table) => {
|
|
174
|
+
const lines = [
|
|
175
|
+
...table.columns.map((column) => prismaField(column, provider)),
|
|
176
|
+
...prismaRelationFields(table, schema),
|
|
177
|
+
...prismaIndexes(table, provider),
|
|
178
|
+
];
|
|
179
|
+
return `model ${table.ormName} {\n${lines
|
|
180
|
+
.map((line) => ` ${line}`)
|
|
181
|
+
.join("\n")}\n}`;
|
|
182
|
+
})
|
|
183
|
+
.join("\n\n")
|
|
184
|
+
);
|
|
185
|
+
};
|
|
170
186
|
|
|
171
187
|
const drizzleImportSource = (provider: ORMSQLProvider) =>
|
|
172
188
|
provider === "sqlite"
|
|
@@ -186,6 +202,18 @@ const drizzleColumnFn = (
|
|
|
186
202
|
column: HotUpdaterColumnSchema,
|
|
187
203
|
provider: ORMSQLProvider,
|
|
188
204
|
): { code: string; imports: readonly string[] } => {
|
|
205
|
+
const collation = column.providerCollations?.[provider];
|
|
206
|
+
if (collation !== undefined) {
|
|
207
|
+
const charset =
|
|
208
|
+
provider === "mysql"
|
|
209
|
+
? ` character set ${collation === "ascii_bin" ? "ascii" : "utf8mb4"}`
|
|
210
|
+
: "";
|
|
211
|
+
const dataType = `${getSqlType(column.type, provider)}${charset} collate ${collation}`;
|
|
212
|
+
return {
|
|
213
|
+
code: `customType<{ data: string }>({ dataType: () => ${literal(dataType)} })(${literal(column.ormName)})`,
|
|
214
|
+
imports: ["customType"],
|
|
215
|
+
};
|
|
216
|
+
}
|
|
189
217
|
if (provider === "sqlite") {
|
|
190
218
|
if (column.type === "bool") {
|
|
191
219
|
return {
|
package/src/db/types.spec.ts
CHANGED
|
@@ -22,9 +22,16 @@ describe("isDatabasePlugin", () => {
|
|
|
22
22
|
bundles: { ...plugin.models.bundles, findMany: null },
|
|
23
23
|
},
|
|
24
24
|
};
|
|
25
|
-
const
|
|
25
|
+
const missingInsights = {
|
|
26
26
|
...plugin,
|
|
27
|
-
models: { ...plugin.models,
|
|
27
|
+
models: { ...plugin.models, insights: undefined },
|
|
28
|
+
};
|
|
29
|
+
const malformedInsights = {
|
|
30
|
+
...plugin,
|
|
31
|
+
models: {
|
|
32
|
+
...plugin.models,
|
|
33
|
+
insights: { ...plugin.models.insights, listEvents: null },
|
|
34
|
+
},
|
|
28
35
|
};
|
|
29
36
|
const malformedChannels = {
|
|
30
37
|
...plugin,
|
|
@@ -47,7 +54,8 @@ describe("isDatabasePlugin", () => {
|
|
|
47
54
|
|
|
48
55
|
expect(isDatabasePlugin(factory)).toBe(false);
|
|
49
56
|
expect(isDatabasePlugin(malformedBundles)).toBe(false);
|
|
50
|
-
expect(isDatabasePlugin(
|
|
57
|
+
expect(isDatabasePlugin(missingInsights)).toBe(false);
|
|
58
|
+
expect(isDatabasePlugin(malformedInsights)).toBe(false);
|
|
51
59
|
expect(isDatabasePlugin(malformedChannels)).toBe(false);
|
|
52
60
|
expect(isDatabasePlugin(malformedApiKeys)).toBe(false);
|
|
53
61
|
expect(isDatabasePlugin(malformedDispose)).toBe(false);
|
package/src/db/types.ts
CHANGED
|
@@ -123,13 +123,19 @@ export function isDatabasePlugin(plugin: unknown): plugin is DatabasePlugin {
|
|
|
123
123
|
typeof plugin.models.channels.list === "function" &&
|
|
124
124
|
"delete" in plugin.models.channels &&
|
|
125
125
|
typeof plugin.models.channels.delete === "function" &&
|
|
126
|
-
"
|
|
127
|
-
typeof plugin.models.
|
|
128
|
-
plugin.models.
|
|
129
|
-
"
|
|
130
|
-
typeof plugin.models.
|
|
131
|
-
"
|
|
132
|
-
typeof plugin.models.
|
|
126
|
+
"insights" in plugin.models &&
|
|
127
|
+
typeof plugin.models.insights === "object" &&
|
|
128
|
+
plugin.models.insights !== null &&
|
|
129
|
+
"record" in plugin.models.insights &&
|
|
130
|
+
typeof plugin.models.insights.record === "function" &&
|
|
131
|
+
"listEvents" in plugin.models.insights &&
|
|
132
|
+
typeof plugin.models.insights.listEvents === "function" &&
|
|
133
|
+
"findInstallations" in plugin.models.insights &&
|
|
134
|
+
typeof plugin.models.insights.findInstallations === "function" &&
|
|
135
|
+
"countEvents" in plugin.models.insights &&
|
|
136
|
+
typeof plugin.models.insights.countEvents === "function" &&
|
|
137
|
+
"countInstallations" in plugin.models.insights &&
|
|
138
|
+
typeof plugin.models.insights.countInstallations === "function" &&
|
|
133
139
|
"apiKeys" in plugin.models &&
|
|
134
140
|
typeof plugin.models.apiKeys === "object" &&
|
|
135
141
|
plugin.models.apiKeys !== null &&
|
|
@@ -156,7 +156,7 @@ const commitTestRelease = async (
|
|
|
156
156
|
};
|
|
157
157
|
|
|
158
158
|
describe("Handler <-> Standalone Repository Integration", () => {
|
|
159
|
-
it("uses Standalone for bundle management and the server database for
|
|
159
|
+
it("uses Standalone for bundle management and the server database for Insights", async () => {
|
|
160
160
|
const bundleId = uuidv7();
|
|
161
161
|
const installId = "standalone-install";
|
|
162
162
|
await createStandaloneClient().insertBundle(
|
|
@@ -186,15 +186,13 @@ describe("Handler <-> Standalone Repository Integration", () => {
|
|
|
186
186
|
}),
|
|
187
187
|
}),
|
|
188
188
|
);
|
|
189
|
-
const
|
|
190
|
-
new Request(`${baseUrl}/installations/overview`),
|
|
191
|
-
);
|
|
189
|
+
const events = await api.handlers.admin(new Request(`${baseUrl}/events`));
|
|
192
190
|
|
|
193
191
|
expect(ingestion.status).toBe(204);
|
|
194
|
-
expect(
|
|
195
|
-
await expect(
|
|
196
|
-
|
|
197
|
-
|
|
192
|
+
expect(events.status).toBe(200);
|
|
193
|
+
await expect(events.json()).resolves.toMatchObject({
|
|
194
|
+
data: [{ installId, toBundleId: bundleId, type: "UPDATE_APPLIED" }],
|
|
195
|
+
nextCursor: null,
|
|
198
196
|
});
|
|
199
197
|
});
|
|
200
198
|
|
package/src/handler.ts
CHANGED
|
@@ -1,9 +1,3 @@
|
|
|
1
|
-
import {
|
|
2
|
-
createAnalyticsRouteHandlers,
|
|
3
|
-
registerAnalyticsAdminRoutes,
|
|
4
|
-
registerAnalyticsClientRoutes,
|
|
5
|
-
} from "./analytics/routes";
|
|
6
|
-
import type { AnalyticsProvider } from "./analytics/types";
|
|
7
1
|
import { createBundleRouteHandlers } from "./handlerBundleRoutes";
|
|
8
2
|
import { HandlerBadRequestError } from "./handlerErrors";
|
|
9
3
|
import { createReleaseCatalogRouteHandlers } from "./handlerReleaseCatalogRoutes";
|
|
@@ -15,6 +9,12 @@ import type {
|
|
|
15
9
|
RouteHandler,
|
|
16
10
|
} from "./handlerTypes";
|
|
17
11
|
import { createVersionRouteHandlers } from "./handlerVersionRoutes";
|
|
12
|
+
import {
|
|
13
|
+
createInsightsRouteHandlers,
|
|
14
|
+
registerInsightsAdminRoutes,
|
|
15
|
+
registerInsightsClientRoutes,
|
|
16
|
+
} from "./insights/routes";
|
|
17
|
+
import type { InsightsProvider } from "./insights/types";
|
|
18
18
|
import { addRoute, createRouter, findRoute } from "./internalRouter";
|
|
19
19
|
|
|
20
20
|
export type {
|
|
@@ -142,7 +142,7 @@ export function createHandlers(api: HandlerAPI): HotUpdaterHandlers {
|
|
|
142
142
|
|
|
143
143
|
export function createHotUpdaterHandlers(
|
|
144
144
|
api: HandlerAPI,
|
|
145
|
-
|
|
145
|
+
insights?: InsightsProvider,
|
|
146
146
|
apiKeyAuth?: {
|
|
147
147
|
readonly authenticate: (request: Request) => Promise<boolean>;
|
|
148
148
|
readonly headerName: string;
|
|
@@ -157,7 +157,7 @@ export function createHotUpdaterHandlers(
|
|
|
157
157
|
...createReleaseCatalogRouteHandlers(apiKeyAuth?.headerName),
|
|
158
158
|
...createReleaseManagementRouteHandlers(),
|
|
159
159
|
...createBundleRouteHandlers(),
|
|
160
|
-
...(
|
|
160
|
+
...(insights === undefined ? {} : createInsightsRouteHandlers(insights)),
|
|
161
161
|
...(downloadStorageObject === undefined
|
|
162
162
|
? {}
|
|
163
163
|
: {
|
|
@@ -196,8 +196,8 @@ export function createHotUpdaterHandlers(
|
|
|
196
196
|
"/artifacts/:targetBundleId/from/:currentBundleId",
|
|
197
197
|
"artifact",
|
|
198
198
|
);
|
|
199
|
-
if (
|
|
200
|
-
|
|
199
|
+
if (insights !== undefined) {
|
|
200
|
+
registerInsightsClientRoutes(addClientRoute);
|
|
201
201
|
}
|
|
202
202
|
|
|
203
203
|
const adminRouter = createRouter<string>();
|
|
@@ -224,8 +224,8 @@ export function createHotUpdaterHandlers(
|
|
|
224
224
|
addAdminRoute("POST", "/bundles", "createBundles");
|
|
225
225
|
addAdminRoute("PATCH", "/bundles/:id", "updateBundle");
|
|
226
226
|
addAdminRoute("DELETE", "/bundles/:id", "deleteBundle");
|
|
227
|
-
if (
|
|
228
|
-
|
|
227
|
+
if (insights !== undefined) {
|
|
228
|
+
registerInsightsAdminRoutes(addAdminRoute);
|
|
229
229
|
}
|
|
230
230
|
|
|
231
231
|
return Object.freeze({
|
package/src/index.ts
CHANGED
|
@@ -4,9 +4,9 @@ export type {
|
|
|
4
4
|
HotUpdaterHandler,
|
|
5
5
|
HotUpdaterHandlers,
|
|
6
6
|
} from "./handler";
|
|
7
|
-
export {
|
|
8
|
-
export type * from "./
|
|
9
|
-
export type
|
|
7
|
+
export { createInsightsProvider } from "./insights/provider";
|
|
8
|
+
export type * from "./insights/domain";
|
|
9
|
+
export type * from "./insights/types";
|
|
10
10
|
export {
|
|
11
11
|
API_KEY_HEADER_NAME,
|
|
12
12
|
createApiKey,
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
export type CreateBundleEventRequestBase = {
|
|
2
|
+
readonly installId: string;
|
|
3
|
+
readonly toBundleId: string;
|
|
4
|
+
readonly userId?: string;
|
|
5
|
+
readonly username?: string;
|
|
6
|
+
readonly platform: "ios" | "android";
|
|
7
|
+
readonly appVersion: string;
|
|
8
|
+
readonly channel: string;
|
|
9
|
+
readonly cohort: string;
|
|
10
|
+
readonly fingerprintHash: string | null;
|
|
11
|
+
readonly sdkVersion?: string | null;
|
|
12
|
+
readonly fromReleaseId: string | null;
|
|
13
|
+
readonly toReleaseId: string | null;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export type CreateBundleEventRequest =
|
|
17
|
+
| (CreateBundleEventRequestBase & {
|
|
18
|
+
readonly type: "UPDATE_APPLIED" | "RECOVERED" | "RELEASE_ADOPTED";
|
|
19
|
+
readonly fromBundleId: string;
|
|
20
|
+
readonly updateStrategy: "fingerprint" | "appVersion";
|
|
21
|
+
})
|
|
22
|
+
| (CreateBundleEventRequestBase & {
|
|
23
|
+
readonly type: "UNCHANGED";
|
|
24
|
+
readonly fromBundleId: null;
|
|
25
|
+
readonly updateStrategy: null;
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
export type ActiveInstallationWindow = "24h" | "7d" | "30d";
|
|
29
|
+
|
|
30
|
+
export type EventHistoryRow = {
|
|
31
|
+
readonly id: string;
|
|
32
|
+
readonly installId: string;
|
|
33
|
+
readonly type:
|
|
34
|
+
| "UPDATE_APPLIED"
|
|
35
|
+
| "RECOVERED"
|
|
36
|
+
| "RELEASE_ADOPTED"
|
|
37
|
+
| "UNCHANGED";
|
|
38
|
+
readonly fromBundleId: string | null;
|
|
39
|
+
readonly toBundleId: string;
|
|
40
|
+
readonly username: string | null;
|
|
41
|
+
readonly userId: string | null;
|
|
42
|
+
readonly platform: "ios" | "android";
|
|
43
|
+
readonly appVersion: string;
|
|
44
|
+
readonly channel: string;
|
|
45
|
+
readonly cohort: string;
|
|
46
|
+
readonly receivedAtMs: number;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export type InstallationHistoryRow = EventHistoryRow & {
|
|
50
|
+
readonly type: "UPDATE_APPLIED" | "RECOVERED";
|
|
51
|
+
readonly fromBundleId: string;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export type InstallationRow = {
|
|
55
|
+
readonly installId: string;
|
|
56
|
+
readonly username: string | null;
|
|
57
|
+
readonly userId: string | null;
|
|
58
|
+
readonly lastKnownBundleId: string;
|
|
59
|
+
readonly latestStatus: EventHistoryRow["type"];
|
|
60
|
+
readonly platform: "ios" | "android";
|
|
61
|
+
readonly appVersion: string;
|
|
62
|
+
readonly channel: string;
|
|
63
|
+
readonly cohort: string;
|
|
64
|
+
readonly receivedAtMs: number;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export type CursorPage<T> = {
|
|
68
|
+
readonly data: readonly T[];
|
|
69
|
+
readonly nextCursor: string | null;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export type EventCursorPage<T extends EventHistoryRow> = CursorPage<T> & {
|
|
73
|
+
readonly beforeReceivedAtMs: number;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export type InsightsScope = {
|
|
77
|
+
readonly platform: "ios" | "android";
|
|
78
|
+
readonly channel: string;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export type InsightsBundleSelection = InsightsScope & {
|
|
82
|
+
readonly bundleId: string;
|
|
83
|
+
readonly outcome: "applied" | "recovered" | "adopted";
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
export type InsightsCountMeasurement = {
|
|
87
|
+
readonly count: number;
|
|
88
|
+
readonly measuredAtMs: number;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
export type ReportingOverview = InsightsScope & {
|
|
92
|
+
readonly window: ActiveInstallationWindow;
|
|
93
|
+
readonly sinceMs: number;
|
|
94
|
+
readonly beforeReceivedAtMs: number;
|
|
95
|
+
readonly reportingInstallations: InsightsCountMeasurement;
|
|
96
|
+
readonly bundle?: {
|
|
97
|
+
readonly bundleId: string;
|
|
98
|
+
readonly reportingInstallations: InsightsCountMeasurement;
|
|
99
|
+
readonly appliedReports: InsightsCountMeasurement;
|
|
100
|
+
readonly recoveredReports: InsightsCountMeasurement;
|
|
101
|
+
readonly adoptedReports: InsightsCountMeasurement;
|
|
102
|
+
};
|
|
103
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export class InsightsBadRequestError extends Error {
|
|
2
|
+
readonly name = "InsightsBadRequestError";
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export class InsightsPayloadTooLargeError extends Error {
|
|
6
|
+
readonly name = "InsightsPayloadTooLargeError";
|
|
7
|
+
|
|
8
|
+
constructor(readonly maximumBytes: number) {
|
|
9
|
+
super(`Event payload exceeds ${maximumBytes} bytes`);
|
|
10
|
+
}
|
|
11
|
+
}
|