@hot-updater/server 1.0.0-rc.0 → 1.0.0-rc.14
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 +14 -1
- package/dist/adapters/databasePluginUtils.mjs +14 -2
- package/dist/adapters/drizzle.cjs +9 -4
- package/dist/adapters/drizzle.d.cts +0 -1
- package/dist/adapters/drizzle.d.mts +0 -1
- package/dist/adapters/drizzle.mjs +10 -4
- package/dist/adapters/drizzleCrud.cjs +93 -3
- package/dist/adapters/drizzleCrud.mjs +93 -5
- package/dist/adapters/drizzleLazyDB.cjs +2 -0
- package/dist/adapters/drizzleLazyDB.mjs +2 -0
- package/dist/adapters/drizzleQuery.cjs +3 -2
- package/dist/adapters/drizzleQuery.mjs +3 -1
- package/dist/adapters/kysely.cjs +1 -1
- package/dist/adapters/kysely.d.cts +0 -1
- package/dist/adapters/kysely.d.mts +0 -1
- package/dist/adapters/kysely.mjs +2 -1
- package/dist/adapters/kyselyCrud.cjs +44 -5
- package/dist/adapters/kyselyCrud.mjs +45 -6
- package/dist/adapters/kyselyQuery.cjs +2 -2
- package/dist/adapters/kyselyQuery.mjs +2 -1
- package/dist/adapters/mongodb.cjs +36 -1
- package/dist/adapters/mongodb.d.cts +1 -1
- package/dist/adapters/mongodb.d.mts +1 -1
- package/dist/adapters/mongodb.mjs +36 -0
- package/dist/adapters/mongodbCollections.cjs +2 -0
- package/dist/adapters/mongodbCollections.mjs +2 -0
- package/dist/adapters/mongodbQuery.cjs +17 -4
- package/dist/adapters/mongodbQuery.mjs +17 -4
- package/dist/adapters/mongodbReads.cjs +88 -30
- package/dist/adapters/mongodbReads.mjs +88 -29
- package/dist/adapters/mongodbWrites.cjs +1 -2
- package/dist/adapters/mongodbWrites.mjs +1 -1
- package/dist/adapters/prisma.cjs +44 -7
- package/dist/adapters/prisma.d.cts +0 -1
- package/dist/adapters/prisma.d.mts +0 -1
- package/dist/adapters/prisma.mjs +44 -6
- package/dist/adapters/prismaInsights.cjs +99 -0
- package/dist/adapters/prismaInsights.mjs +97 -0
- package/dist/adapters/prismaQuery.cjs +0 -1
- package/dist/adapters/prismaRows.cjs +7 -8
- package/dist/adapters/prismaRows.mjs +8 -8
- 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 +29 -9
- package/dist/db/schemaGenerators.mjs +29 -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 +92 -0
- package/dist/insights/domain.d.mts +92 -0
- package/dist/insights/errors.cjs +15 -0
- package/dist/insights/errors.mjs +14 -0
- package/dist/{analytics → insights}/eventInput.cjs +61 -16
- package/dist/{analytics → insights}/eventInput.mjs +61 -18
- package/dist/insights/provider.cjs +332 -0
- package/dist/insights/provider.d.cts +6 -0
- package/dist/insights/provider.d.mts +6 -0
- package/dist/insights/provider.mjs +332 -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 +64 -35
- package/dist/schema/v1_0_0.mjs +64 -36
- 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 +15 -5
- package/src/adapters/databasePluginUtils.ts +24 -0
- package/src/adapters/drizzle.spec.ts +203 -5
- package/src/adapters/drizzle.ts +16 -4
- package/src/adapters/drizzleCrud.ts +200 -9
- package/src/adapters/drizzleLazyDB.ts +33 -6
- package/src/adapters/kysely.spec.ts +62 -0
- package/src/adapters/kysely.ts +7 -1
- package/src/adapters/kyselyCrud.ts +115 -7
- package/src/adapters/kyselyInsights.spec.ts +168 -0
- package/src/adapters/mongodb.integration.spec.ts +483 -0
- package/src/adapters/mongodb.spec.ts +89 -0
- package/src/adapters/mongodb.ts +59 -1
- package/src/adapters/mongodbCollections.ts +16 -0
- package/src/adapters/mongodbQuery.ts +22 -3
- package/src/adapters/mongodbReads.ts +78 -2
- package/src/adapters/mongodbTestClient.ts +107 -6
- package/src/adapters/mongodbTestFilter.ts +5 -1
- package/src/adapters/mongodbWrites.ts +1 -0
- package/src/adapters/prisma.spec.ts +270 -0
- package/src/adapters/prisma.ts +75 -2
- package/src/adapters/prismaInsights.ts +190 -0
- package/src/adapters/prismaRows.ts +17 -10
- package/src/adapters/prismaTestClient.ts +89 -7
- package/src/apiKeys.integration.spec.ts +3 -3
- package/src/createHotUpdaterCore.ts +30 -16
- package/src/db/databasePluginUpdateCheck.bench.ts +10 -0
- package/src/db/fixedMigrator.spec.ts +63 -0
- package/src/db/fixedMigratorMongo.spec.ts +70 -9
- package/src/db/fixedMigratorMongo.ts +10 -10
- package/src/db/index.spec.ts +6 -1
- package/src/db/schema/insightsCollation.ts +21 -0
- package/src/db/schema/sql.ts +8 -5
- package/src/db/schemaGenerators.ts +42 -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 +106 -0
- package/src/insights/errors.ts +11 -0
- package/src/{analytics → insights}/eventInput.ts +66 -16
- package/src/insights/insights.integration.spec.ts +475 -0
- package/src/insights/provider.spec.ts +396 -0
- package/src/insights/provider.ts +611 -0
- package/src/insights/queryInput.ts +142 -0
- package/src/insights/routes.ts +104 -0
- package/src/insights/types.ts +55 -0
- package/src/schema/dsl-all-versions.spec.ts +1 -1
- package/src/schema/v1_0_0.spec.ts +77 -3
- package/src/schema/v1_0_0.ts +83 -37
- 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
|
@@ -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
|
|
|
@@ -28,6 +30,7 @@ const toPascalCase = (value: string): string =>
|
|
|
28
30
|
.join("");
|
|
29
31
|
|
|
30
32
|
const prismaDb = (type: HotUpdaterColumnType, provider: ORMProvider) => {
|
|
33
|
+
if (provider === "cockroachdb" && type === "uuid") return " @db.Uuid";
|
|
31
34
|
if (provider === "postgresql") {
|
|
32
35
|
if (type === "uuid") return " @db.Uuid";
|
|
33
36
|
if (type.startsWith("varchar")) {
|
|
@@ -154,19 +157,33 @@ const prismaIndexes = (
|
|
|
154
157
|
export const generatePrismaSchema = (
|
|
155
158
|
provider: ORMProvider,
|
|
156
159
|
schema: HotUpdaterVersionedSchema = hotUpdaterSchema,
|
|
157
|
-
) =>
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
.join("\n")
|
|
168
|
-
|
|
169
|
-
|
|
160
|
+
) => {
|
|
161
|
+
const sqlProvider = getSQLProvider(provider);
|
|
162
|
+
const collationSql = sqlProvider ? getInsightsCollationSql(sqlProvider) : [];
|
|
163
|
+
const header =
|
|
164
|
+
collationSql.length === 0
|
|
165
|
+
? ""
|
|
166
|
+
: [
|
|
167
|
+
"// Apply these statements in the initial Prisma SQL migration to preserve exact Insights identities and cursors:",
|
|
168
|
+
...collationSql.map((statement) => `// ${statement};`),
|
|
169
|
+
"",
|
|
170
|
+
].join("\n");
|
|
171
|
+
return (
|
|
172
|
+
header +
|
|
173
|
+
schema.tables
|
|
174
|
+
.map((table) => {
|
|
175
|
+
const lines = [
|
|
176
|
+
...table.columns.map((column) => prismaField(column, provider)),
|
|
177
|
+
...prismaRelationFields(table, schema),
|
|
178
|
+
...prismaIndexes(table, provider),
|
|
179
|
+
];
|
|
180
|
+
return `model ${table.ormName} {\n${lines
|
|
181
|
+
.map((line) => ` ${line}`)
|
|
182
|
+
.join("\n")}\n}`;
|
|
183
|
+
})
|
|
184
|
+
.join("\n\n")
|
|
185
|
+
);
|
|
186
|
+
};
|
|
170
187
|
|
|
171
188
|
const drizzleImportSource = (provider: ORMSQLProvider) =>
|
|
172
189
|
provider === "sqlite"
|
|
@@ -186,6 +203,18 @@ const drizzleColumnFn = (
|
|
|
186
203
|
column: HotUpdaterColumnSchema,
|
|
187
204
|
provider: ORMSQLProvider,
|
|
188
205
|
): { code: string; imports: readonly string[] } => {
|
|
206
|
+
const collation = column.providerCollations?.[provider];
|
|
207
|
+
if (collation !== undefined) {
|
|
208
|
+
const charset =
|
|
209
|
+
provider === "mysql"
|
|
210
|
+
? ` character set ${collation === "ascii_bin" ? "ascii" : "utf8mb4"}`
|
|
211
|
+
: "";
|
|
212
|
+
const dataType = `${getSqlType(column.type, provider)}${charset} collate ${collation}`;
|
|
213
|
+
return {
|
|
214
|
+
code: `customType<{ data: string }>({ dataType: () => ${literal(dataType)} })(${literal(column.ormName)})`,
|
|
215
|
+
imports: ["customType"],
|
|
216
|
+
};
|
|
217
|
+
}
|
|
189
218
|
if (provider === "sqlite") {
|
|
190
219
|
if (column.type === "bool") {
|
|
191
220
|
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
|
+
"recordEvent" in plugin.models.insights &&
|
|
130
|
+
typeof plugin.models.insights.recordEvent === "function" &&
|
|
131
|
+
"listEvents" in plugin.models.insights &&
|
|
132
|
+
typeof plugin.models.insights.listEvents === "function" &&
|
|
133
|
+
"findLatestEvents" in plugin.models.insights &&
|
|
134
|
+
typeof plugin.models.insights.findLatestEvents === "function" &&
|
|
135
|
+
"countEvents" in plugin.models.insights &&
|
|
136
|
+
typeof plugin.models.insights.countEvents === "function" &&
|
|
137
|
+
"countLatestEvents" in plugin.models.insights &&
|
|
138
|
+
typeof plugin.models.insights.countLatestEvents === "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,106 @@
|
|
|
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_DOWNLOADED" | "UPDATE_APPLIED" | "RECOVERED";
|
|
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_DOWNLOADED"
|
|
35
|
+
| "UPDATE_APPLIED"
|
|
36
|
+
| "RECOVERED"
|
|
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_DOWNLOADED" | "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 pendingBundleId: string | null;
|
|
60
|
+
readonly pendingReleaseId: string | null;
|
|
61
|
+
readonly latestStatus: EventHistoryRow["type"];
|
|
62
|
+
readonly platform: "ios" | "android";
|
|
63
|
+
readonly appVersion: string;
|
|
64
|
+
readonly channel: string;
|
|
65
|
+
readonly cohort: string;
|
|
66
|
+
readonly receivedAtMs: number;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export type CursorPage<T> = {
|
|
70
|
+
readonly data: readonly T[];
|
|
71
|
+
readonly nextCursor: string | null;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export type EventCursorPage<T extends EventHistoryRow> = CursorPage<T> & {
|
|
75
|
+
readonly beforeReceivedAtMs: number;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export type InsightsScope = {
|
|
79
|
+
readonly platform: "ios" | "android";
|
|
80
|
+
readonly channel: string;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
export type InsightsBundleSelection = InsightsScope & {
|
|
84
|
+
readonly bundleId: string;
|
|
85
|
+
readonly outcome: "downloaded" | "applied" | "recovered" | "unchanged";
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
export type InsightsCountMeasurement = {
|
|
89
|
+
readonly count: number;
|
|
90
|
+
readonly measuredAtMs: number;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
export type ReportingOverview = InsightsScope & {
|
|
94
|
+
readonly window: ActiveInstallationWindow;
|
|
95
|
+
readonly sinceMs: number;
|
|
96
|
+
readonly beforeReceivedAtMs: number;
|
|
97
|
+
readonly reportingInstallations: InsightsCountMeasurement;
|
|
98
|
+
readonly bundle?: {
|
|
99
|
+
readonly bundleId: string;
|
|
100
|
+
readonly reportingInstallations: InsightsCountMeasurement;
|
|
101
|
+
readonly downloadedReports: InsightsCountMeasurement;
|
|
102
|
+
readonly appliedReports: InsightsCountMeasurement;
|
|
103
|
+
readonly recoveredReports: InsightsCountMeasurement;
|
|
104
|
+
readonly unchangedReports: InsightsCountMeasurement;
|
|
105
|
+
};
|
|
106
|
+
};
|
|
@@ -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
|
+
}
|
|
@@ -1,13 +1,16 @@
|
|
|
1
|
+
import { createUUIDv7, type BundleEventRow } from "@hot-updater/plugin-core";
|
|
2
|
+
|
|
1
3
|
import type {
|
|
2
4
|
CreateBundleEventRequest,
|
|
3
5
|
CreateBundleEventRequestBase,
|
|
4
6
|
} from "./domain";
|
|
5
7
|
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
+
InsightsBadRequestError,
|
|
9
|
+
InsightsPayloadTooLargeError,
|
|
8
10
|
} from "./errors";
|
|
9
11
|
|
|
10
12
|
const MAX_EVENT_STRING_LENGTH = 1_024;
|
|
13
|
+
const MAX_IDENTITY_LENGTH = 255;
|
|
11
14
|
export const EVENT_BODY_MAX_BYTES = 16 * 1_024;
|
|
12
15
|
|
|
13
16
|
const eventKeys = new Set([
|
|
@@ -40,9 +43,12 @@ function requireStringField(
|
|
|
40
43
|
if (
|
|
41
44
|
typeof value !== "string" ||
|
|
42
45
|
value.length === 0 ||
|
|
43
|
-
value.length > MAX_EVENT_STRING_LENGTH
|
|
46
|
+
value.length > MAX_EVENT_STRING_LENGTH ||
|
|
47
|
+
new TextDecoder("utf-8", { ignoreBOM: true }).decode(
|
|
48
|
+
new TextEncoder().encode(value),
|
|
49
|
+
) !== value
|
|
44
50
|
) {
|
|
45
|
-
throw new
|
|
51
|
+
throw new InsightsBadRequestError(`Invalid event field: ${key}`);
|
|
46
52
|
}
|
|
47
53
|
return value;
|
|
48
54
|
}
|
|
@@ -55,6 +61,17 @@ function requireNullableStringField(
|
|
|
55
61
|
return requireStringField(payload, key);
|
|
56
62
|
}
|
|
57
63
|
|
|
64
|
+
function requireIdentityField(
|
|
65
|
+
payload: Readonly<Record<string, unknown>>,
|
|
66
|
+
key: "installId" | "userId",
|
|
67
|
+
): string {
|
|
68
|
+
const value = requireStringField(payload, key);
|
|
69
|
+
if (value.length > MAX_IDENTITY_LENGTH) {
|
|
70
|
+
throw new InsightsBadRequestError(`Invalid event field: ${key}`);
|
|
71
|
+
}
|
|
72
|
+
return value;
|
|
73
|
+
}
|
|
74
|
+
|
|
58
75
|
async function readBoundedText(request: Request): Promise<string> {
|
|
59
76
|
const contentLength = request.headers.get("content-length");
|
|
60
77
|
const declaredByteLength = Number(contentLength);
|
|
@@ -63,7 +80,7 @@ async function readBoundedText(request: Request): Promise<string> {
|
|
|
63
80
|
Number.isSafeInteger(declaredByteLength) &&
|
|
64
81
|
declaredByteLength > EVENT_BODY_MAX_BYTES
|
|
65
82
|
) {
|
|
66
|
-
throw new
|
|
83
|
+
throw new InsightsPayloadTooLargeError(EVENT_BODY_MAX_BYTES);
|
|
67
84
|
}
|
|
68
85
|
if (request.body === null) return "";
|
|
69
86
|
const reader = request.body.getReader();
|
|
@@ -76,7 +93,7 @@ async function readBoundedText(request: Request): Promise<string> {
|
|
|
76
93
|
byteLength += result.value.byteLength;
|
|
77
94
|
if (byteLength > EVENT_BODY_MAX_BYTES) {
|
|
78
95
|
await reader.cancel();
|
|
79
|
-
throw new
|
|
96
|
+
throw new InsightsPayloadTooLargeError(EVENT_BODY_MAX_BYTES);
|
|
80
97
|
}
|
|
81
98
|
text += decoder.decode(result.value, { stream: true });
|
|
82
99
|
}
|
|
@@ -89,7 +106,7 @@ async function parseJson(request: Request): Promise<unknown> {
|
|
|
89
106
|
return JSON.parse(text);
|
|
90
107
|
} catch (error) {
|
|
91
108
|
if (error instanceof SyntaxError) {
|
|
92
|
-
throw new
|
|
109
|
+
throw new InsightsBadRequestError("Invalid event payload");
|
|
93
110
|
}
|
|
94
111
|
throw error;
|
|
95
112
|
}
|
|
@@ -100,18 +117,18 @@ function requireEvent(payload: unknown): CreateBundleEventRequest {
|
|
|
100
117
|
!isRecord(payload) ||
|
|
101
118
|
Object.keys(payload).some((key) => !eventKeys.has(key))
|
|
102
119
|
) {
|
|
103
|
-
throw new
|
|
120
|
+
throw new InsightsBadRequestError("Invalid event payload");
|
|
104
121
|
}
|
|
105
122
|
const platform = requireStringField(payload, "platform");
|
|
106
123
|
if (platform !== "ios" && platform !== "android") {
|
|
107
|
-
throw new
|
|
124
|
+
throw new InsightsBadRequestError("Invalid event field: platform");
|
|
108
125
|
}
|
|
109
126
|
const base: CreateBundleEventRequestBase = {
|
|
110
|
-
installId:
|
|
127
|
+
installId: requireIdentityField(payload, "installId"),
|
|
111
128
|
toBundleId: requireStringField(payload, "toBundleId"),
|
|
112
129
|
...(payload.userId === undefined
|
|
113
130
|
? {}
|
|
114
|
-
: { userId:
|
|
131
|
+
: { userId: requireIdentityField(payload, "userId") }),
|
|
115
132
|
...(payload.username === undefined
|
|
116
133
|
? {}
|
|
117
134
|
: { username: requireStringField(payload, "username") }),
|
|
@@ -129,12 +146,12 @@ function requireEvent(payload: unknown): CreateBundleEventRequest {
|
|
|
129
146
|
};
|
|
130
147
|
const type = requireStringField(payload, "type");
|
|
131
148
|
switch (type) {
|
|
149
|
+
case "UPDATE_DOWNLOADED":
|
|
132
150
|
case "UPDATE_APPLIED":
|
|
133
|
-
case "RECOVERED":
|
|
134
|
-
case "RELEASE_ADOPTED": {
|
|
151
|
+
case "RECOVERED": {
|
|
135
152
|
const updateStrategy = requireStringField(payload, "updateStrategy");
|
|
136
153
|
if (updateStrategy !== "fingerprint" && updateStrategy !== "appVersion") {
|
|
137
|
-
throw new
|
|
154
|
+
throw new InsightsBadRequestError(
|
|
138
155
|
"Invalid event field: updateStrategy",
|
|
139
156
|
);
|
|
140
157
|
}
|
|
@@ -147,7 +164,7 @@ function requireEvent(payload: unknown): CreateBundleEventRequest {
|
|
|
147
164
|
}
|
|
148
165
|
case "UNCHANGED":
|
|
149
166
|
if (payload.fromBundleId !== null || payload.updateStrategy !== null) {
|
|
150
|
-
throw new
|
|
167
|
+
throw new InsightsBadRequestError("Invalid unchanged event shape");
|
|
151
168
|
}
|
|
152
169
|
return {
|
|
153
170
|
...base,
|
|
@@ -156,7 +173,7 @@ function requireEvent(payload: unknown): CreateBundleEventRequest {
|
|
|
156
173
|
updateStrategy: null,
|
|
157
174
|
};
|
|
158
175
|
default:
|
|
159
|
-
throw new
|
|
176
|
+
throw new InsightsBadRequestError("Invalid event field: type");
|
|
160
177
|
}
|
|
161
178
|
}
|
|
162
179
|
|
|
@@ -166,3 +183,36 @@ export async function parseBundleEventRequest(
|
|
|
166
183
|
const payload = await parseJson(request);
|
|
167
184
|
return requireEvent(payload);
|
|
168
185
|
}
|
|
186
|
+
|
|
187
|
+
export function createBundleEventRow(
|
|
188
|
+
input: CreateBundleEventRequest,
|
|
189
|
+
): BundleEventRow {
|
|
190
|
+
input = requireEvent(input);
|
|
191
|
+
const base = {
|
|
192
|
+
app_version: input.appVersion,
|
|
193
|
+
channel: input.channel,
|
|
194
|
+
from_release_id: input.fromReleaseId,
|
|
195
|
+
id: createUUIDv7(),
|
|
196
|
+
install_id: input.installId,
|
|
197
|
+
platform: input.platform,
|
|
198
|
+
received_at_ms: Date.now(),
|
|
199
|
+
to_bundle_id: input.toBundleId,
|
|
200
|
+
to_release_id: input.toReleaseId,
|
|
201
|
+
user_id: input.userId ?? null,
|
|
202
|
+
metadata: {
|
|
203
|
+
cohort: input.cohort,
|
|
204
|
+
fingerprint_hash: input.fingerprintHash,
|
|
205
|
+
sdk_version: input.sdkVersion ?? null,
|
|
206
|
+
username: input.username ?? null,
|
|
207
|
+
update_strategy: input.updateStrategy,
|
|
208
|
+
},
|
|
209
|
+
};
|
|
210
|
+
switch (input.type) {
|
|
211
|
+
case "UPDATE_DOWNLOADED":
|
|
212
|
+
case "UPDATE_APPLIED":
|
|
213
|
+
case "RECOVERED":
|
|
214
|
+
return { ...base, from_bundle_id: input.fromBundleId, type: input.type };
|
|
215
|
+
case "UNCHANGED":
|
|
216
|
+
return { ...base, from_bundle_id: null, type: input.type };
|
|
217
|
+
}
|
|
218
|
+
}
|