@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
|
@@ -3,6 +3,7 @@ import type {
|
|
|
3
3
|
BundlePatchRow,
|
|
4
4
|
ChannelRow,
|
|
5
5
|
ApiKeyRow,
|
|
6
|
+
InsightsInstallationRow,
|
|
6
7
|
ReleaseCatalogRow,
|
|
7
8
|
} from "@hot-updater/plugin-core";
|
|
8
9
|
|
|
@@ -13,10 +14,14 @@ export type DrizzleTable = Record<string, unknown>;
|
|
|
13
14
|
|
|
14
15
|
type DrizzleMutation = {
|
|
15
16
|
readonly execute: () => Promise<unknown>;
|
|
17
|
+
readonly all?: () => unknown[];
|
|
18
|
+
readonly run?: () => unknown;
|
|
16
19
|
};
|
|
17
20
|
|
|
18
21
|
export type DrizzleInsertMutation = DrizzleMutation & {
|
|
19
|
-
readonly
|
|
22
|
+
readonly onDuplicateKeyUpdate?: (config: { set: object }) => DrizzleMutation;
|
|
23
|
+
readonly onConflictDoNothing?: () => DrizzleInsertMutation;
|
|
24
|
+
readonly returning?: (fields: Record<string, unknown>) => DrizzleMutation;
|
|
20
25
|
};
|
|
21
26
|
|
|
22
27
|
type DrizzleInsertBuilder = {
|
|
@@ -30,6 +35,7 @@ type DrizzleQuery<TRow> = {
|
|
|
30
35
|
};
|
|
31
36
|
|
|
32
37
|
export type DrizzleDB = {
|
|
38
|
+
readonly resultKind?: "sync" | "async";
|
|
33
39
|
readonly _: { readonly fullSchema: Record<string, DrizzleTable> };
|
|
34
40
|
readonly $count: (table: DrizzleTable, where?: unknown) => Promise<number>;
|
|
35
41
|
readonly delete: (table: DrizzleTable) => {
|
|
@@ -41,6 +47,7 @@ export type DrizzleDB = {
|
|
|
41
47
|
readonly channels: DrizzleQuery<ChannelRow>;
|
|
42
48
|
readonly bundle_patches: DrizzleQuery<BundlePatchRow>;
|
|
43
49
|
readonly bundle_events: DrizzleQuery<BundleEventRow>;
|
|
50
|
+
readonly bundle_installations: DrizzleQuery<InsightsInstallationRow>;
|
|
44
51
|
readonly api_keys: DrizzleQuery<ApiKeyRow>;
|
|
45
52
|
readonly releases: DrizzleQuery<StoredReleaseRow>;
|
|
46
53
|
readonly release_catalogs: DrizzleQuery<ReleaseCatalogRow>;
|
|
@@ -51,7 +58,7 @@ export type DrizzleDB = {
|
|
|
51
58
|
};
|
|
52
59
|
};
|
|
53
60
|
readonly transaction?: <TResult>(
|
|
54
|
-
operation: (transaction: DrizzleDB) => Promise<TResult>,
|
|
61
|
+
operation: (transaction: DrizzleDB) => TResult | Promise<TResult>,
|
|
55
62
|
) => Promise<TResult>;
|
|
56
63
|
};
|
|
57
64
|
|
|
@@ -82,6 +89,7 @@ const isDrizzleDB = (value: unknown): value is DrizzleDB => {
|
|
|
82
89
|
if (
|
|
83
90
|
!isRecord(query) ||
|
|
84
91
|
!isDrizzleQuery(query["bundle_events"]) ||
|
|
92
|
+
!isDrizzleQuery(query["bundle_installations"]) ||
|
|
85
93
|
!isDrizzleQuery(query["bundle_patches"]) ||
|
|
86
94
|
!isDrizzleQuery(query["bundles"]) ||
|
|
87
95
|
!isDrizzleQuery(query["channels"]) ||
|
|
@@ -193,6 +201,12 @@ export const createLazyDB = (config: DrizzleConfig): DrizzleDB => {
|
|
|
193
201
|
findMany: async (args) =>
|
|
194
202
|
(await getDB()).query.bundle_events.findMany(args),
|
|
195
203
|
},
|
|
204
|
+
bundle_installations: {
|
|
205
|
+
findFirst: async (args) =>
|
|
206
|
+
(await getDB()).query.bundle_installations.findFirst(args),
|
|
207
|
+
findMany: async (args) =>
|
|
208
|
+
(await getDB()).query.bundle_installations.findMany(args),
|
|
209
|
+
},
|
|
196
210
|
bundle_patches: {
|
|
197
211
|
findFirst: async (args) =>
|
|
198
212
|
(await getDB()).query.bundle_patches.findFirst(args),
|
|
@@ -241,7 +255,7 @@ export const createLazyDB = (config: DrizzleConfig): DrizzleDB => {
|
|
|
241
255
|
...(config.transaction === true
|
|
242
256
|
? {
|
|
243
257
|
transaction: async <TResult>(
|
|
244
|
-
operation: (transaction: DrizzleDB) => Promise<TResult>,
|
|
258
|
+
operation: (transaction: DrizzleDB) => TResult | Promise<TResult>,
|
|
245
259
|
): Promise<TResult> => {
|
|
246
260
|
const db = await getDB();
|
|
247
261
|
if (db.transaction === undefined) {
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
const isRetryableConflict = (error: unknown): boolean => {
|
|
2
|
+
if (typeof error !== "object" || error === null) return false;
|
|
3
|
+
if (
|
|
4
|
+
"code" in error &&
|
|
5
|
+
[
|
|
6
|
+
"ER_DUP_ENTRY",
|
|
7
|
+
"ER_LOCK_DEADLOCK",
|
|
8
|
+
"ER_LOCK_WAIT_TIMEOUT",
|
|
9
|
+
"40001",
|
|
10
|
+
"40P01",
|
|
11
|
+
].includes(String(error.code))
|
|
12
|
+
)
|
|
13
|
+
return true;
|
|
14
|
+
return (
|
|
15
|
+
"cause" in error &&
|
|
16
|
+
error.cause !== error &&
|
|
17
|
+
isRetryableConflict(error.cause)
|
|
18
|
+
);
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const runInsightsTransaction = async (
|
|
22
|
+
operation: () => Promise<void>,
|
|
23
|
+
): Promise<void> => {
|
|
24
|
+
for (let attempt = 0; ; attempt += 1) {
|
|
25
|
+
try {
|
|
26
|
+
await operation();
|
|
27
|
+
return;
|
|
28
|
+
} catch (error) {
|
|
29
|
+
if (attempt >= 2 || !isRetryableConflict(error)) throw error;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
};
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { PGlite } from "@electric-sql/pglite";
|
|
2
|
+
import { toInsightsInstallationRow } from "@hot-updater/plugin-core";
|
|
2
3
|
import { Kysely } from "kysely";
|
|
3
4
|
import { PGliteDialect } from "kysely-pglite-dialect";
|
|
4
5
|
import { describe, expect, it } from "vitest";
|
|
5
6
|
|
|
6
7
|
import {
|
|
7
8
|
createBundlePatchRowFixture,
|
|
9
|
+
createBundleEventRowFixture,
|
|
8
10
|
createBundleRowFixture,
|
|
9
11
|
createChannelRowFixture,
|
|
10
12
|
} from "../../../test-utils/src/databaseTestFixtures";
|
|
@@ -54,6 +56,33 @@ setupDatabasePluginTestSuite({
|
|
|
54
56
|
});
|
|
55
57
|
|
|
56
58
|
describe("kyselyAdapter SQLite JSON storage", () => {
|
|
59
|
+
it("rolls back the event when the installation write fails and permits retry", async () => {
|
|
60
|
+
const db = new PGlite();
|
|
61
|
+
const kysely = new Kysely<object>({ dialect: new PGliteDialect(db) });
|
|
62
|
+
await db.exec(DATABASE_PLUGIN_TEST_SCHEMA_SQL);
|
|
63
|
+
await db.exec(
|
|
64
|
+
"alter table bundle_installations add constraint reject_snapshot check (install_id <> 'install-703')",
|
|
65
|
+
);
|
|
66
|
+
const plugin = kyselyAdapter({ db: kysely, provider: "postgresql" });
|
|
67
|
+
const event = createBundleEventRowFixture("703", 100);
|
|
68
|
+
const input = { event, installation: toInsightsInstallationRow(event) };
|
|
69
|
+
try {
|
|
70
|
+
await expect(plugin.models.insights.record(input)).rejects.toThrow();
|
|
71
|
+
expect((await db.query("select id from bundle_events")).rows).toEqual([]);
|
|
72
|
+
await db.exec(
|
|
73
|
+
"alter table bundle_installations drop constraint reject_snapshot",
|
|
74
|
+
);
|
|
75
|
+
await plugin.models.insights.record(input);
|
|
76
|
+
await expect(
|
|
77
|
+
plugin.models.insights.findInstallations({
|
|
78
|
+
installId: event.install_id,
|
|
79
|
+
}),
|
|
80
|
+
).resolves.toEqual([input.installation]);
|
|
81
|
+
} finally {
|
|
82
|
+
await kysely.destroy();
|
|
83
|
+
await db.close();
|
|
84
|
+
}
|
|
85
|
+
});
|
|
57
86
|
it("round-trips JSON values through text columns", async () => {
|
|
58
87
|
const sqliteClient = new PGlite();
|
|
59
88
|
const sqliteDatabase = new Kysely<object>({
|
package/src/adapters/kysely.ts
CHANGED
|
@@ -11,7 +11,8 @@ import type {
|
|
|
11
11
|
ORMSQLProvider,
|
|
12
12
|
RelationMode,
|
|
13
13
|
} from "../db/types";
|
|
14
|
-
import {
|
|
14
|
+
import { runInsightsTransaction } from "./insightsTransaction";
|
|
15
|
+
import { createKyselyCrud, recordKyselyInsights } from "./kyselyCrud";
|
|
15
16
|
|
|
16
17
|
type KyselySQLProvider = Exclude<ORMSQLProvider, "mssql">;
|
|
17
18
|
|
|
@@ -31,6 +32,14 @@ const createImplementation = <TDatabase extends object>(
|
|
|
31
32
|
const crud = createKyselyCrud(db, config.provider, relationMode);
|
|
32
33
|
return {
|
|
33
34
|
...crud,
|
|
35
|
+
recordInsights: (input) =>
|
|
36
|
+
runInsightsTransaction(() =>
|
|
37
|
+
db
|
|
38
|
+
.transaction()
|
|
39
|
+
.execute((transaction) =>
|
|
40
|
+
recordKyselyInsights(transaction, config.provider, input),
|
|
41
|
+
),
|
|
42
|
+
),
|
|
34
43
|
deleteChannel: (input) =>
|
|
35
44
|
db
|
|
36
45
|
.transaction()
|
|
@@ -4,6 +4,8 @@ import {
|
|
|
4
4
|
type BundlePatchRow,
|
|
5
5
|
type ChannelRow,
|
|
6
6
|
type ApiKeyRow,
|
|
7
|
+
type InsightsInstallationRow,
|
|
8
|
+
type InsightsRecordInput,
|
|
7
9
|
type ReleaseCatalogRow,
|
|
8
10
|
} from "@hot-updater/plugin-core";
|
|
9
11
|
import type {
|
|
@@ -54,6 +56,7 @@ const whereClause = (
|
|
|
54
56
|
const orderClause = (
|
|
55
57
|
input:
|
|
56
58
|
| {
|
|
59
|
+
readonly model?: string;
|
|
57
60
|
readonly orderBy?: readonly {
|
|
58
61
|
readonly direction: "asc" | "desc";
|
|
59
62
|
readonly field: string;
|
|
@@ -67,6 +70,14 @@ const orderClause = (
|
|
|
67
70
|
return sql` order by ${sql.join(
|
|
68
71
|
clauses.map((clause) => {
|
|
69
72
|
const field = sql.ref(clause.field);
|
|
73
|
+
if (
|
|
74
|
+
input?.model === "bundle_events" ||
|
|
75
|
+
input?.model === "bundle_installations"
|
|
76
|
+
) {
|
|
77
|
+
return clause.direction === "asc"
|
|
78
|
+
? sql`${field} asc`
|
|
79
|
+
: sql`${field} desc`;
|
|
80
|
+
}
|
|
70
81
|
const nulls =
|
|
71
82
|
clause.nulls ?? (clause.direction === "asc" ? "last" : "first");
|
|
72
83
|
const nullOrder =
|
|
@@ -110,6 +121,46 @@ const insertRow = async (
|
|
|
110
121
|
return Number(result.numAffectedRows ?? 0) > 0;
|
|
111
122
|
};
|
|
112
123
|
|
|
124
|
+
export const recordKyselyInsights = async (
|
|
125
|
+
executor: QueryExecutorProvider,
|
|
126
|
+
provider: Exclude<ORMSQLProvider, "mssql">,
|
|
127
|
+
{ event, installation }: InsightsRecordInput,
|
|
128
|
+
): Promise<void> => {
|
|
129
|
+
const insert = (table: string, row: object) => {
|
|
130
|
+
const entries = Object.entries(row);
|
|
131
|
+
return sql`insert into ${sql.table(table)} (${sql.join(entries.map(([field]) => sql.ref(field)))}) values (${sql.join(entries.map(([, value]) => value))})`;
|
|
132
|
+
};
|
|
133
|
+
if (provider === "mysql") {
|
|
134
|
+
const existing = await sql<{
|
|
135
|
+
id: string;
|
|
136
|
+
}>`select id from bundle_events where id = ${event.id} for update`.execute(
|
|
137
|
+
executor,
|
|
138
|
+
);
|
|
139
|
+
if (existing.rows.length > 0) return;
|
|
140
|
+
await insert("bundle_events", event).execute(executor);
|
|
141
|
+
await sql`${insert("bundle_installations", installation)} on duplicate key update install_id = ${installation.install_id}`.execute(
|
|
142
|
+
executor,
|
|
143
|
+
);
|
|
144
|
+
} else {
|
|
145
|
+
const result =
|
|
146
|
+
await sql`${insert("bundle_events", event)} on conflict (id) do nothing returning id`.execute(
|
|
147
|
+
executor,
|
|
148
|
+
);
|
|
149
|
+
if (result.rows.length === 0) return;
|
|
150
|
+
await sql`${insert("bundle_installations", installation)} on conflict (install_id) do nothing`.execute(
|
|
151
|
+
executor,
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
const assignments = Object.entries(installation).map(
|
|
155
|
+
([field, value]) => sql`${sql.ref(field)} = ${value}`,
|
|
156
|
+
);
|
|
157
|
+
await sql`update ${sql.table("bundle_installations")} set ${sql.join(assignments)}
|
|
158
|
+
where ${sql.ref("install_id")} = ${installation.install_id}
|
|
159
|
+
and (${sql.ref("received_at_ms")} < ${installation.received_at_ms}
|
|
160
|
+
or (${sql.ref("received_at_ms")} = ${installation.received_at_ms}
|
|
161
|
+
and ${sql.ref("id")} < ${installation.id}))`.execute(executor);
|
|
162
|
+
};
|
|
163
|
+
|
|
113
164
|
const updateBundle = async (
|
|
114
165
|
executor: QueryExecutorProvider,
|
|
115
166
|
id: string,
|
|
@@ -298,6 +349,15 @@ export const createKyselyCrud = (
|
|
|
298
349
|
case "bundle_events":
|
|
299
350
|
await insertRow(executor, "bundle_events", input.data, provider);
|
|
300
351
|
return input.data;
|
|
352
|
+
case "bundle_installations":
|
|
353
|
+
await insertRow(
|
|
354
|
+
executor,
|
|
355
|
+
"bundle_installations",
|
|
356
|
+
input.data,
|
|
357
|
+
provider,
|
|
358
|
+
input.onConflict,
|
|
359
|
+
);
|
|
360
|
+
return input.data;
|
|
301
361
|
case "releases":
|
|
302
362
|
await insertRow(
|
|
303
363
|
executor,
|
|
@@ -330,6 +390,38 @@ export const createKyselyCrud = (
|
|
|
330
390
|
}
|
|
331
391
|
},
|
|
332
392
|
async update(input) {
|
|
393
|
+
if (input.model === "bundle_installations") {
|
|
394
|
+
const installId = input.where.find(
|
|
395
|
+
(item) =>
|
|
396
|
+
item.field === "install_id" &&
|
|
397
|
+
(item.operator === undefined || item.operator === "eq") &&
|
|
398
|
+
typeof item.value === "string",
|
|
399
|
+
)?.value;
|
|
400
|
+
if (typeof installId !== "string") {
|
|
401
|
+
throw new KyselyAdapterInvariantError(
|
|
402
|
+
"bundle_installations.update.selector",
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
const assignments = Object.entries(input.update)
|
|
406
|
+
.filter(([, value]) => value !== undefined)
|
|
407
|
+
.map(([field, value]) => sql`${sql.ref(field)} = ${value}`);
|
|
408
|
+
const where = buildKyselyWhere(provider, input.where);
|
|
409
|
+
let updated = false;
|
|
410
|
+
if (assignments.length > 0 && where !== undefined) {
|
|
411
|
+
const result = await sql`update ${sql.table(
|
|
412
|
+
"bundle_installations",
|
|
413
|
+
)} set ${sql.join(assignments)} where ${where}`.execute(executor);
|
|
414
|
+
updated = Number(result.numAffectedRows ?? 0) > 0;
|
|
415
|
+
}
|
|
416
|
+
if (!updated) return null;
|
|
417
|
+
const result =
|
|
418
|
+
await sql<InsightsInstallationRow>`select * from ${sql.table(
|
|
419
|
+
"bundle_installations",
|
|
420
|
+
)} where ${sql.ref("install_id")} = ${installId} limit 1`.execute(
|
|
421
|
+
executor,
|
|
422
|
+
);
|
|
423
|
+
return result.rows[0] ?? null;
|
|
424
|
+
}
|
|
333
425
|
const selector = input.where[0];
|
|
334
426
|
if (selector === undefined || typeof selector.value !== "string") {
|
|
335
427
|
throw new KyselyAdapterInvariantError(`${input.model}.update.selector`);
|
|
@@ -452,6 +544,12 @@ export const createKyselyCrud = (
|
|
|
452
544
|
throw new DatabasePluginInputError("invalid-operation");
|
|
453
545
|
}
|
|
454
546
|
switch (input.model) {
|
|
547
|
+
case "bundle_events":
|
|
548
|
+
return countRows(
|
|
549
|
+
executor,
|
|
550
|
+
"bundle_events",
|
|
551
|
+
buildKyselyWhere(provider, input.where),
|
|
552
|
+
);
|
|
455
553
|
case "bundles":
|
|
456
554
|
return countRows(
|
|
457
555
|
executor,
|
|
@@ -470,6 +568,12 @@ export const createKyselyCrud = (
|
|
|
470
568
|
"releases",
|
|
471
569
|
buildKyselyWhere(provider, input.where),
|
|
472
570
|
);
|
|
571
|
+
case "bundle_installations":
|
|
572
|
+
return countRows(
|
|
573
|
+
executor,
|
|
574
|
+
"bundle_installations",
|
|
575
|
+
buildKyselyWhere(provider, input.where),
|
|
576
|
+
);
|
|
473
577
|
}
|
|
474
578
|
},
|
|
475
579
|
async findOne(input) {
|
|
@@ -519,6 +623,14 @@ export const createKyselyCrud = (
|
|
|
519
623
|
const row = result.rows[0];
|
|
520
624
|
return row === undefined ? null : fromStoredReleaseCatalogRow(row);
|
|
521
625
|
}
|
|
626
|
+
case "bundle_installations": {
|
|
627
|
+
const where = whereClause(buildKyselyWhere(provider, input.where));
|
|
628
|
+
const result =
|
|
629
|
+
await sql<InsightsInstallationRow>`select * from ${sql.table(
|
|
630
|
+
"bundle_installations",
|
|
631
|
+
)}${where} limit 1`.execute(executor);
|
|
632
|
+
return result.rows[0] ?? null;
|
|
633
|
+
}
|
|
522
634
|
}
|
|
523
635
|
},
|
|
524
636
|
async findMany(input) {
|
|
@@ -551,6 +663,15 @@ export const createKyselyCrud = (
|
|
|
551
663
|
)}${where}${order}${pagination}`.execute(executor);
|
|
552
664
|
return [...result.rows];
|
|
553
665
|
}
|
|
666
|
+
case "bundle_installations": {
|
|
667
|
+
const where = whereClause(buildKyselyWhere(provider, input.where));
|
|
668
|
+
const order = orderClause(input);
|
|
669
|
+
const result =
|
|
670
|
+
await sql<InsightsInstallationRow>`select * from ${sql.table(
|
|
671
|
+
"bundle_installations",
|
|
672
|
+
)}${where}${order}${pagination}`.execute(executor);
|
|
673
|
+
return [...result.rows];
|
|
674
|
+
}
|
|
554
675
|
case "api_keys": {
|
|
555
676
|
const where = whereClause(buildKyselyWhere(provider, input.where));
|
|
556
677
|
const order = orderClause(input);
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import net from "node:net";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
toInsightsInstallationRow,
|
|
6
|
+
type BundleEventRow,
|
|
7
|
+
} from "@hot-updater/plugin-core";
|
|
8
|
+
import { execa } from "execa";
|
|
9
|
+
import { MongoClient } from "mongodb";
|
|
10
|
+
import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
|
11
|
+
|
|
12
|
+
import { createBundleEventRowFixture } from "../../../test-utils/src/databaseTestFixtures";
|
|
13
|
+
import { mongoAdapter } from "./mongodb";
|
|
14
|
+
|
|
15
|
+
const availablePort = (): Promise<number> =>
|
|
16
|
+
new Promise((resolve, reject) => {
|
|
17
|
+
const server = net.createServer();
|
|
18
|
+
server.once("error", reject);
|
|
19
|
+
server.listen(0, "127.0.0.1", () => {
|
|
20
|
+
const address = server.address();
|
|
21
|
+
if (address === null || typeof address === "string") {
|
|
22
|
+
server.close();
|
|
23
|
+
reject(new Error("Failed to allocate MongoDB test port"));
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
server.close((error) => (error ? reject(error) : resolve(address.port)));
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe("MongoDB native Insights transactions", () => {
|
|
31
|
+
let client: MongoClient;
|
|
32
|
+
let composeEnvironment: Record<string, string>;
|
|
33
|
+
const composeFile = path.resolve(
|
|
34
|
+
import.meta.dirname,
|
|
35
|
+
"../../../../examples-server/hono-mongodb/docker-compose.yml",
|
|
36
|
+
);
|
|
37
|
+
const compose = (args: string[]) =>
|
|
38
|
+
execa("docker", ["compose", "-f", composeFile, ...args], {
|
|
39
|
+
env: composeEnvironment,
|
|
40
|
+
});
|
|
41
|
+
const insights = () => mongoAdapter({ client }).models.insights;
|
|
42
|
+
const record = (event: BundleEventRow) =>
|
|
43
|
+
insights().record({
|
|
44
|
+
event,
|
|
45
|
+
installation: toInsightsInstallationRow(event),
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
beforeAll(async () => {
|
|
49
|
+
const port = await availablePort();
|
|
50
|
+
composeEnvironment = {
|
|
51
|
+
COMPOSE_PROJECT_NAME: `hot-updater-insights-${process.pid}`,
|
|
52
|
+
HOT_UPDATER_E2E_MONGODB_PORT: String(port),
|
|
53
|
+
};
|
|
54
|
+
await compose(["up", "-d", "--wait"]);
|
|
55
|
+
client = new MongoClient(
|
|
56
|
+
`mongodb://127.0.0.1:${port}/insights_native?replicaSet=rs0&directConnection=true`,
|
|
57
|
+
);
|
|
58
|
+
await client.connect();
|
|
59
|
+
const adapter = mongoAdapter({ client });
|
|
60
|
+
await (await adapter.createMigrator!().migrateToLatest()).execute();
|
|
61
|
+
}, 120_000);
|
|
62
|
+
|
|
63
|
+
beforeEach(async () => {
|
|
64
|
+
await client.db().collection("bundle_events").deleteMany({});
|
|
65
|
+
await client.db().collection("bundle_installations").deleteMany({});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
afterAll(async () => {
|
|
69
|
+
await client?.close();
|
|
70
|
+
if (composeEnvironment) await compose(["down", "-v", "--remove-orphans"]);
|
|
71
|
+
}, 60_000);
|
|
72
|
+
|
|
73
|
+
it("rolls back both canonical changes after a native installation validation error", async () => {
|
|
74
|
+
const previous = {
|
|
75
|
+
...createBundleEventRowFixture("801", 100),
|
|
76
|
+
user_id: "original",
|
|
77
|
+
};
|
|
78
|
+
await record(previous);
|
|
79
|
+
await client.db().command({
|
|
80
|
+
collMod: "bundle_installations",
|
|
81
|
+
validator: { user_id: { $ne: "reject" } },
|
|
82
|
+
});
|
|
83
|
+
const failed = {
|
|
84
|
+
...createBundleEventRowFixture("802", 200),
|
|
85
|
+
install_id: previous.install_id,
|
|
86
|
+
user_id: "reject",
|
|
87
|
+
};
|
|
88
|
+
try {
|
|
89
|
+
await expect(record(failed)).rejects.toThrow();
|
|
90
|
+
expect(
|
|
91
|
+
await client.db().collection("bundle_events").countDocuments({}),
|
|
92
|
+
).toBe(1);
|
|
93
|
+
await expect(
|
|
94
|
+
insights().findInstallations({ installId: previous.install_id }),
|
|
95
|
+
).resolves.toEqual([toInsightsInstallationRow(previous)]);
|
|
96
|
+
} finally {
|
|
97
|
+
await client
|
|
98
|
+
.db()
|
|
99
|
+
.command({ collMod: "bundle_installations", validator: {} });
|
|
100
|
+
}
|
|
101
|
+
await record(failed);
|
|
102
|
+
await record(failed);
|
|
103
|
+
expect(
|
|
104
|
+
await client.db().collection("bundle_events").countDocuments({}),
|
|
105
|
+
).toBe(2);
|
|
106
|
+
await expect(
|
|
107
|
+
insights().findInstallations({ installId: previous.install_id }),
|
|
108
|
+
).resolves.toEqual([toInsightsInstallationRow(failed)]);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("retries native conflicts without dropping events or letting duplicate IDs rewrite state", async () => {
|
|
112
|
+
const events = Array.from({ length: 12 }, (_, index) => ({
|
|
113
|
+
...createBundleEventRowFixture(String(810 + index), 100),
|
|
114
|
+
install_id: "concurrent-installation",
|
|
115
|
+
user_id: index === 11 ? null : "old-user",
|
|
116
|
+
}));
|
|
117
|
+
await Promise.all(events.map(record));
|
|
118
|
+
const winner = events.at(-1)!;
|
|
119
|
+
await Promise.all(Array.from({ length: 4 }, () => record(winner)));
|
|
120
|
+
const conflictingDuplicate = {
|
|
121
|
+
...events[0]!,
|
|
122
|
+
install_id: "different-installation",
|
|
123
|
+
received_at_ms: 1000,
|
|
124
|
+
};
|
|
125
|
+
await record(conflictingDuplicate);
|
|
126
|
+
expect(
|
|
127
|
+
await client.db().collection("bundle_events").countDocuments({}),
|
|
128
|
+
).toBe(12);
|
|
129
|
+
await expect(
|
|
130
|
+
insights().findInstallations({ installId: winner.install_id }),
|
|
131
|
+
).resolves.toEqual([toInsightsInstallationRow(winner)]);
|
|
132
|
+
await expect(
|
|
133
|
+
insights().findInstallations({
|
|
134
|
+
installId: conflictingDuplicate.install_id,
|
|
135
|
+
}),
|
|
136
|
+
).resolves.toEqual([]);
|
|
137
|
+
await expect(
|
|
138
|
+
insights().findInstallations({ userId: "old-user", limit: 10 }),
|
|
139
|
+
).resolves.toEqual([]);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("accepts concurrent retries of a previously unseen event ID exactly once", async () => {
|
|
143
|
+
const event = createBundleEventRowFixture("825", 100);
|
|
144
|
+
await Promise.all(Array.from({ length: 12 }, () => record(event)));
|
|
145
|
+
expect(
|
|
146
|
+
await client.db().collection("bundle_events").countDocuments({}),
|
|
147
|
+
).toBe(1);
|
|
148
|
+
await expect(
|
|
149
|
+
insights().findInstallations({ installId: event.install_id }),
|
|
150
|
+
).resolves.toEqual([toInsightsInstallationRow(event)]);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("uses raw recovery predicates consistently in native counts and indexed lists", async () => {
|
|
154
|
+
const applied = createBundleEventRowFixture("831", 100);
|
|
155
|
+
const recovered: BundleEventRow = {
|
|
156
|
+
...createBundleEventRowFixture("832", 200),
|
|
157
|
+
type: "RECOVERED",
|
|
158
|
+
update_strategy: "appVersion",
|
|
159
|
+
install_id: applied.install_id,
|
|
160
|
+
from_bundle_id: applied.to_bundle_id,
|
|
161
|
+
to_bundle_id: applied.from_bundle_id!,
|
|
162
|
+
};
|
|
163
|
+
await record(applied);
|
|
164
|
+
await record(recovered);
|
|
165
|
+
const filter = {
|
|
166
|
+
type: "RECOVERED" as const,
|
|
167
|
+
platform: "ios" as const,
|
|
168
|
+
channel: "production",
|
|
169
|
+
fromBundleId: applied.to_bundle_id,
|
|
170
|
+
};
|
|
171
|
+
await expect(
|
|
172
|
+
insights().countEvents({ filter, sinceMs: 200, beforeReceivedAtMs: 201 }),
|
|
173
|
+
).resolves.toBe(1);
|
|
174
|
+
await expect(
|
|
175
|
+
insights().listEvents({
|
|
176
|
+
filter: { kind: "bundle", ...filter },
|
|
177
|
+
sinceMs: 200,
|
|
178
|
+
beforeReceivedAtMs: 201,
|
|
179
|
+
limit: 10,
|
|
180
|
+
}),
|
|
181
|
+
).resolves.toEqual([recovered]);
|
|
182
|
+
await expect(
|
|
183
|
+
insights().countEvents({ filter, sinceMs: 100, beforeReceivedAtMs: 200 }),
|
|
184
|
+
).resolves.toBe(0);
|
|
185
|
+
await expect(
|
|
186
|
+
insights().countInstallations({
|
|
187
|
+
platform: "ios",
|
|
188
|
+
channel: "production",
|
|
189
|
+
bundleId: applied.to_bundle_id,
|
|
190
|
+
sinceMs: 0,
|
|
191
|
+
}),
|
|
192
|
+
).resolves.toBe(0);
|
|
193
|
+
const explanation = await client
|
|
194
|
+
.db()
|
|
195
|
+
.collection("bundle_events")
|
|
196
|
+
.find({
|
|
197
|
+
type: "RECOVERED",
|
|
198
|
+
platform: "ios",
|
|
199
|
+
channel: "production",
|
|
200
|
+
from_bundle_id: applied.to_bundle_id,
|
|
201
|
+
received_at_ms: { $gte: 200, $lt: 201 },
|
|
202
|
+
})
|
|
203
|
+
.collation({ locale: "simple" })
|
|
204
|
+
.sort({ received_at_ms: -1, id: -1 })
|
|
205
|
+
.hint("bundle_events_from_bundle_idx")
|
|
206
|
+
.explain("executionStats");
|
|
207
|
+
expect(explanation.executionStats.nReturned).toBe(1);
|
|
208
|
+
expect(explanation.executionStats.totalKeysExamined).toBeLessThanOrEqual(2);
|
|
209
|
+
});
|
|
210
|
+
});
|
|
@@ -1,9 +1,13 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
createDatabaseClient,
|
|
3
|
+
toInsightsInstallationRow,
|
|
4
|
+
} from "@hot-updater/plugin-core";
|
|
2
5
|
import { describe, expect, it } from "vitest";
|
|
3
6
|
|
|
4
7
|
import {
|
|
5
8
|
createBundlePatchRowFixture,
|
|
6
9
|
createBundleRowFixture,
|
|
10
|
+
createBundleEventRowFixture,
|
|
7
11
|
} from "../../../test-utils/src/databaseTestFixtures";
|
|
8
12
|
import { setupDatabasePluginTestSuite } from "../../../test-utils/src/setupDatabasePluginTestSuite";
|
|
9
13
|
import { mongoAdapter } from "./mongodb";
|
|
@@ -22,6 +26,70 @@ setupDatabasePluginTestSuite({
|
|
|
22
26
|
});
|
|
23
27
|
|
|
24
28
|
describe("mongoAdapter capabilities", () => {
|
|
29
|
+
it("records Insights atomically even without legacy catalog transactions enabled", async () => {
|
|
30
|
+
harness.reset();
|
|
31
|
+
const insights = mongoAdapter({ client: harness.client }).models.insights;
|
|
32
|
+
const event = createBundleEventRowFixture("981", 100);
|
|
33
|
+
const input = { event, installation: toInsightsInstallationRow(event) };
|
|
34
|
+
harness.failNextInstallationWrite();
|
|
35
|
+
await expect(insights.record(input)).rejects.toThrow(
|
|
36
|
+
"injected installation write failure",
|
|
37
|
+
);
|
|
38
|
+
await expect(
|
|
39
|
+
insights.listEvents({
|
|
40
|
+
filter: { kind: "all" },
|
|
41
|
+
beforeReceivedAtMs: 200,
|
|
42
|
+
limit: 10,
|
|
43
|
+
}),
|
|
44
|
+
).resolves.toEqual([]);
|
|
45
|
+
await expect(
|
|
46
|
+
insights.findInstallations({ installId: event.install_id }),
|
|
47
|
+
).resolves.toEqual([]);
|
|
48
|
+
await insights.record(input);
|
|
49
|
+
await insights.record(input);
|
|
50
|
+
await expect(
|
|
51
|
+
insights.listEvents({
|
|
52
|
+
filter: { kind: "all" },
|
|
53
|
+
beforeReceivedAtMs: 200,
|
|
54
|
+
limit: 10,
|
|
55
|
+
}),
|
|
56
|
+
).resolves.toEqual([event]);
|
|
57
|
+
await expect(
|
|
58
|
+
insights.findInstallations({ installId: event.install_id }),
|
|
59
|
+
).resolves.toEqual([input.installation]);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("preserves all concurrent events while advancing one installation and clearing its user", async () => {
|
|
63
|
+
harness.reset();
|
|
64
|
+
const insights = mongoAdapter({ client: harness.client }).models.insights;
|
|
65
|
+
const events = ["990", "993", "991", "992"].map((suffix) => ({
|
|
66
|
+
...createBundleEventRowFixture(suffix, 100),
|
|
67
|
+
install_id: "concurrent-installation",
|
|
68
|
+
user_id: suffix === "993" ? null : "previous-user",
|
|
69
|
+
}));
|
|
70
|
+
await Promise.all(
|
|
71
|
+
events.map((event) =>
|
|
72
|
+
insights.record({
|
|
73
|
+
event,
|
|
74
|
+
installation: toInsightsInstallationRow(event),
|
|
75
|
+
}),
|
|
76
|
+
),
|
|
77
|
+
);
|
|
78
|
+
await expect(
|
|
79
|
+
insights.findInstallations({ installId: "concurrent-installation" }),
|
|
80
|
+
).resolves.toEqual([toInsightsInstallationRow(events[1]!)]);
|
|
81
|
+
await expect(
|
|
82
|
+
insights.findInstallations({ userId: "previous-user", limit: 10 }),
|
|
83
|
+
).resolves.toEqual([]);
|
|
84
|
+
await expect(
|
|
85
|
+
insights.listEvents({
|
|
86
|
+
filter: { kind: "all" },
|
|
87
|
+
beforeReceivedAtMs: 200,
|
|
88
|
+
limit: 10,
|
|
89
|
+
}),
|
|
90
|
+
).resolves.toHaveLength(4);
|
|
91
|
+
});
|
|
92
|
+
|
|
25
93
|
it("returns an adapter without an unsafe atomic-batch fallback", () => {
|
|
26
94
|
const plugin = mongoAdapter({ client: harness.client });
|
|
27
95
|
expect(plugin.name).toBe("mongodb");
|