@hot-updater/cloudflare 1.0.0-rc.5 → 1.0.1
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/index.cjs +90 -924
- package/dist/index.d.cts +16 -6
- package/dist/index.d.mts +16 -6
- package/dist/index.mjs +91 -926
- package/dist/init/index.cjs +11777 -0
- package/dist/init/index.d.cts +5 -1
- package/dist/init/index.d.mts +5 -1
- package/dist/init/index.mjs +11771 -1
- package/dist/worker/index.cjs +117 -916
- package/dist/worker/index.d.cts +45 -11
- package/dist/worker/index.d.mts +45 -11
- package/dist/worker/index.mjs +118 -917
- package/package.json +9 -14
- package/sql/bundles.sql +121 -212
- package/src/d1Database.ts +58 -43
- package/src/d1Executor.ts +84 -0
- package/src/d1Schema.ts +26 -0
- package/src/presignR2Url.ts +103 -0
- package/src/r2S3Storage.ts +20 -19
- package/src/r2Storage.ts +5 -3
- package/src/r2WorkerStorage.ts +55 -17
- package/src/worker/d1Database.ts +21 -39
- package/worker/dist/README.md +1 -1
- package/worker/dist/index.js +9859 -5696
- package/worker/dist/index.js.map +4 -4
- package/worker/migrations/0001_hot-updater_1.0.0.sql +121 -222
- package/worker/src/index.ts +15 -9
- package/worker/wrangler.json +1 -0
- package/dist/iac/index.cjs +0 -12687
- package/dist/iac/index.d.cts +0 -5
- package/dist/iac/index.d.mts +0 -5
- package/dist/iac/index.mjs +0 -12679
- package/src/d1Database.spec.ts +0 -277
- package/src/d1Implementation.spec.ts +0 -342
- package/src/d1Implementation.ts +0 -781
- package/src/d1Query.spec.ts +0 -46
- package/src/d1Query.ts +0 -92
- package/src/d1Rows.spec.ts +0 -143
- package/src/d1Rows.ts +0 -357
- package/src/d1Sql.spec.ts +0 -30
- package/src/d1Sql.ts +0 -175
- package/src/r2Storage.spec.ts +0 -346
- package/src/r2WorkerStorage.spec.ts +0 -184
- package/src/utils/createWrangler.spec.ts +0 -75
- package/src/worker/d1Database.spec.ts +0 -69
- package/worker/src/databaseMigration.integration.spec.ts +0 -191
- package/worker/src/databasePlugins.integration.spec.ts +0 -326
- package/worker/src/index.integration.spec.ts +0 -190
- package/worker/src/insightsMovement.integration.spec.ts +0 -73
- package/worker/src/storagePlugin.integration.spec.ts +0 -59
- package/worker/src/testWorker.ts +0 -5
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
-
|
|
3
|
-
const mocks = vi.hoisted(() => ({
|
|
4
|
-
command: vi.fn(),
|
|
5
|
-
execa: vi.fn(),
|
|
6
|
-
}));
|
|
7
|
-
|
|
8
|
-
vi.mock("execa", () => ({
|
|
9
|
-
execa: mocks.execa,
|
|
10
|
-
}));
|
|
11
|
-
|
|
12
|
-
import { createWrangler } from "./createWrangler";
|
|
13
|
-
|
|
14
|
-
describe("createWrangler", () => {
|
|
15
|
-
beforeEach(() => {
|
|
16
|
-
vi.clearAllMocks();
|
|
17
|
-
mocks.execa.mockReturnValue(mocks.command);
|
|
18
|
-
mocks.command.mockResolvedValue({});
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
it("marks replay commands as CI so Wrangler skips nested prompts", async () => {
|
|
22
|
-
const wrangler = createWrangler({
|
|
23
|
-
accountId: "account-id",
|
|
24
|
-
cloudflareApiToken: "api-token",
|
|
25
|
-
cwd: "/tmp/cloudflare-init",
|
|
26
|
-
nonInteractive: true,
|
|
27
|
-
stdio: "inherit",
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
await wrangler("d1", "migrations", "apply", "hot-updater", "--remote");
|
|
31
|
-
|
|
32
|
-
expect(mocks.execa).toHaveBeenCalledWith({
|
|
33
|
-
cwd: "/tmp/cloudflare-init",
|
|
34
|
-
env: {
|
|
35
|
-
CI: "true",
|
|
36
|
-
CLOUDFLARE_ACCOUNT_ID: "account-id",
|
|
37
|
-
CLOUDFLARE_API_TOKEN: "api-token",
|
|
38
|
-
},
|
|
39
|
-
extendsEnv: true,
|
|
40
|
-
shell: false,
|
|
41
|
-
stdio: "inherit",
|
|
42
|
-
});
|
|
43
|
-
expect(mocks.command).toHaveBeenCalledWith("npx", [
|
|
44
|
-
"wrangler",
|
|
45
|
-
"d1",
|
|
46
|
-
"migrations",
|
|
47
|
-
"apply",
|
|
48
|
-
"hot-updater",
|
|
49
|
-
"--remote",
|
|
50
|
-
]);
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
it("passes shell metacharacters as one argument", async () => {
|
|
54
|
-
const wrangler = createWrangler({
|
|
55
|
-
accountId: "account-id",
|
|
56
|
-
cloudflareApiToken: "api-token",
|
|
57
|
-
cwd: "/tmp/cloudflare-init",
|
|
58
|
-
stdio: "inherit",
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
await wrangler("deploy", "--name", "safe; printf INJECTED");
|
|
62
|
-
|
|
63
|
-
expect(mocks.execa).toHaveBeenCalledWith(
|
|
64
|
-
expect.objectContaining({
|
|
65
|
-
shell: false,
|
|
66
|
-
}),
|
|
67
|
-
);
|
|
68
|
-
expect(mocks.command).toHaveBeenCalledWith("npx", [
|
|
69
|
-
"wrangler",
|
|
70
|
-
"deploy",
|
|
71
|
-
"--name",
|
|
72
|
-
"safe; printf INJECTED",
|
|
73
|
-
]);
|
|
74
|
-
});
|
|
75
|
-
});
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
import { toInsightsInstallationRow } from "@hot-updater/plugin-core";
|
|
2
|
-
import { expect, it } from "vitest";
|
|
3
|
-
|
|
4
|
-
import { createBundleEventRowFixture } from "../../../../packages/test-utils/src/databaseTestFixtures";
|
|
5
|
-
import { d1Database, type D1Like } from "./d1Database";
|
|
6
|
-
|
|
7
|
-
const database: D1Like = {
|
|
8
|
-
prepare: () => ({
|
|
9
|
-
bind: () => ({
|
|
10
|
-
all: async () => ({
|
|
11
|
-
results: [{ id: "channel-production", name: "production" }],
|
|
12
|
-
}),
|
|
13
|
-
}),
|
|
14
|
-
}),
|
|
15
|
-
batch: async (statements) =>
|
|
16
|
-
Promise.all(statements.map((item) => item.all())),
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
it("uses the D1 binding supplied at the Worker composition boundary", async () => {
|
|
20
|
-
const plugin = d1Database(database);
|
|
21
|
-
|
|
22
|
-
expect(plugin.name).toBe("d1Database");
|
|
23
|
-
expect(Object.keys(plugin.models).sort()).toEqual([
|
|
24
|
-
"apiKeys",
|
|
25
|
-
"bundlePatches",
|
|
26
|
-
"bundles",
|
|
27
|
-
"channels",
|
|
28
|
-
"insights",
|
|
29
|
-
"releaseCatalogs",
|
|
30
|
-
"releases",
|
|
31
|
-
]);
|
|
32
|
-
await expect(plugin.models.channels.list({})).resolves.toEqual({
|
|
33
|
-
channels: [{ id: "channel-production", name: "production" }],
|
|
34
|
-
});
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
it("does not report a failed D1 query as empty Insights history", async () => {
|
|
38
|
-
const plugin = d1Database({
|
|
39
|
-
...database,
|
|
40
|
-
prepare: () => ({
|
|
41
|
-
bind: () => ({ all: async () => ({ success: false, results: [] }) }),
|
|
42
|
-
}),
|
|
43
|
-
});
|
|
44
|
-
await expect(
|
|
45
|
-
plugin.models.insights.listEvents({
|
|
46
|
-
filter: { kind: "all" },
|
|
47
|
-
beforeReceivedAtMs: 100,
|
|
48
|
-
limit: 10,
|
|
49
|
-
}),
|
|
50
|
-
).rejects.toThrow(
|
|
51
|
-
"D1 did not successfully execute every requested statement",
|
|
52
|
-
);
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it("rejects an incomplete Insights batch response", async () => {
|
|
56
|
-
const plugin = d1Database({
|
|
57
|
-
...database,
|
|
58
|
-
batch: async () => [{ success: true, results: [] }],
|
|
59
|
-
});
|
|
60
|
-
const event = createBundleEventRowFixture("1", 1);
|
|
61
|
-
await expect(
|
|
62
|
-
plugin.models.insights.record({
|
|
63
|
-
event,
|
|
64
|
-
installation: toInsightsInstallationRow(event),
|
|
65
|
-
}),
|
|
66
|
-
).rejects.toThrow(
|
|
67
|
-
"D1 did not successfully execute every requested statement",
|
|
68
|
-
);
|
|
69
|
-
});
|
|
@@ -1,191 +0,0 @@
|
|
|
1
|
-
import { env } from "cloudflare:test";
|
|
2
|
-
import { expect, inject, it } from "vitest";
|
|
3
|
-
|
|
4
|
-
declare module "vitest" {
|
|
5
|
-
export interface ProvidedContext {
|
|
6
|
-
d1Migrations: readonly {
|
|
7
|
-
readonly name: string;
|
|
8
|
-
readonly sql: string;
|
|
9
|
-
}[];
|
|
10
|
-
}
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
it("ships a single 1.0.0 initialization migration", () => {
|
|
14
|
-
expect(inject("d1Migrations").map(({ name }) => name)).toEqual([
|
|
15
|
-
"0001_hot-updater_1.0.0.sql",
|
|
16
|
-
]);
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
it("creates the current schema with required artifact sizes", async () => {
|
|
20
|
-
const [createMigration] = inject("d1Migrations");
|
|
21
|
-
await env.DB.prepare(createMigration!.sql).run();
|
|
22
|
-
await env.DB.prepare(`
|
|
23
|
-
INSERT INTO bundles (
|
|
24
|
-
id, platform, file_hash, storage_uri, archive_byte_size, metadata
|
|
25
|
-
) VALUES (
|
|
26
|
-
'00000000-0000-0000-0000-000000000001', 'ios', 'hash',
|
|
27
|
-
'storage://bundle', 3000000001, '{}'
|
|
28
|
-
)
|
|
29
|
-
`).run();
|
|
30
|
-
await env.DB.prepare(`
|
|
31
|
-
INSERT INTO bundle_patches (
|
|
32
|
-
id, bundle_id, base_bundle_id, base_file_hash, patch_file_hash,
|
|
33
|
-
patch_storage_uri, byte_size
|
|
34
|
-
) VALUES (
|
|
35
|
-
'patch-1', '00000000-0000-0000-0000-000000000001',
|
|
36
|
-
'00000000-0000-0000-0000-000000000001', 'base-hash', 'patch-hash',
|
|
37
|
-
'storage://patch', 3000000002
|
|
38
|
-
)
|
|
39
|
-
`).run();
|
|
40
|
-
|
|
41
|
-
const tables = await env.DB.prepare(`
|
|
42
|
-
SELECT name FROM sqlite_master
|
|
43
|
-
WHERE type = 'table'
|
|
44
|
-
ORDER BY name
|
|
45
|
-
`).all<{ name: string }>();
|
|
46
|
-
|
|
47
|
-
expect(tables.results.map(({ name }) => name)).toEqual(
|
|
48
|
-
expect.arrayContaining([
|
|
49
|
-
"bundle_events",
|
|
50
|
-
"bundle_installations",
|
|
51
|
-
"bundle_patches",
|
|
52
|
-
"bundles",
|
|
53
|
-
"channels",
|
|
54
|
-
"api_keys",
|
|
55
|
-
"private_hot_updater_settings",
|
|
56
|
-
"release_catalogs",
|
|
57
|
-
"releases",
|
|
58
|
-
]),
|
|
59
|
-
);
|
|
60
|
-
|
|
61
|
-
const version = await env.DB.prepare(
|
|
62
|
-
"SELECT value FROM private_hot_updater_settings WHERE key = 'schema.core'",
|
|
63
|
-
).first<string>("value");
|
|
64
|
-
expect(version).toBe("1.0.0");
|
|
65
|
-
|
|
66
|
-
await env.DB.prepare(
|
|
67
|
-
"INSERT INTO channels (id, name) VALUES ('channel-1', 'production')",
|
|
68
|
-
).run();
|
|
69
|
-
await env.DB.prepare(`
|
|
70
|
-
INSERT INTO releases (
|
|
71
|
-
id, revision, scope_key, channel_id, platform, kind, bundle_id,
|
|
72
|
-
strategy, target_app_version, fingerprint_hash, enabled,
|
|
73
|
-
should_force_update, message, rollout_cohort_count, target_cohorts,
|
|
74
|
-
operation, source_release_id, created_at_ms, updated_at_ms
|
|
75
|
-
) VALUES (
|
|
76
|
-
'00000000-0000-0000-0000-000000000001', 1, 'scope', 'channel-1',
|
|
77
|
-
'ios', 'BUNDLE', '00000000-0000-0000-0000-000000000001',
|
|
78
|
-
'APP_VERSION', '1.0.0', NULL, 1, 0, NULL, 1000, '[]',
|
|
79
|
-
'DEPLOY', NULL, 0, 0
|
|
80
|
-
)
|
|
81
|
-
`).run();
|
|
82
|
-
|
|
83
|
-
const release = await env.DB.prepare(
|
|
84
|
-
"SELECT channel_id, bundle_id FROM releases",
|
|
85
|
-
).first();
|
|
86
|
-
expect(release).toEqual({
|
|
87
|
-
channel_id: "channel-1",
|
|
88
|
-
bundle_id: "00000000-0000-0000-0000-000000000001",
|
|
89
|
-
});
|
|
90
|
-
const sizes = await env.DB.prepare(`
|
|
91
|
-
SELECT bundle.archive_byte_size, patch.byte_size
|
|
92
|
-
FROM bundles AS bundle
|
|
93
|
-
JOIN bundle_patches AS patch ON patch.bundle_id = bundle.id
|
|
94
|
-
`).first();
|
|
95
|
-
expect(sizes).toEqual({
|
|
96
|
-
archive_byte_size: 3_000_000_001,
|
|
97
|
-
byte_size: 3_000_000_002,
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
await env.DB.prepare(`
|
|
101
|
-
INSERT INTO bundle_installations (
|
|
102
|
-
install_id, id, user_id, username, to_bundle_id, type, platform,
|
|
103
|
-
app_version, channel, cohort, received_at_ms
|
|
104
|
-
) VALUES (
|
|
105
|
-
'install-1', 'event-1', 'user-1', 'Demo User',
|
|
106
|
-
'00000000-0000-0000-0000-000000000001', 'UPDATE_APPLIED', 'ios',
|
|
107
|
-
'1.0.0', 'production', 'cohort-1', 100
|
|
108
|
-
)
|
|
109
|
-
`).run();
|
|
110
|
-
await expect(
|
|
111
|
-
env.DB.prepare(
|
|
112
|
-
"SELECT install_id, received_at_ms FROM bundle_installations",
|
|
113
|
-
).first(),
|
|
114
|
-
).resolves.toEqual({ install_id: "install-1", received_at_ms: 100 });
|
|
115
|
-
|
|
116
|
-
const installationIndexes = await env.DB.prepare(`
|
|
117
|
-
SELECT name FROM sqlite_master
|
|
118
|
-
WHERE type = 'index' AND tbl_name = 'bundle_installations'
|
|
119
|
-
ORDER BY name
|
|
120
|
-
`).all<{ name: string }>();
|
|
121
|
-
expect(installationIndexes.results.map(({ name }) => name)).toEqual(
|
|
122
|
-
expect.arrayContaining([
|
|
123
|
-
"bundle_installations_received_at_idx",
|
|
124
|
-
"bundle_installations_user_id_idx",
|
|
125
|
-
"bundle_installations_scope_idx",
|
|
126
|
-
"bundle_installations_bundle_idx",
|
|
127
|
-
]),
|
|
128
|
-
);
|
|
129
|
-
|
|
130
|
-
const movementIndex = await env.DB.prepare(
|
|
131
|
-
"PRAGMA index_info(bundle_events_install_idx)",
|
|
132
|
-
).all<{ name: string }>();
|
|
133
|
-
expect(movementIndex.results.map(({ name }) => name)).toEqual([
|
|
134
|
-
"install_id",
|
|
135
|
-
"type",
|
|
136
|
-
"received_at_ms",
|
|
137
|
-
"id",
|
|
138
|
-
]);
|
|
139
|
-
|
|
140
|
-
for (const [direction, type] of [
|
|
141
|
-
["from", "RECOVERED"],
|
|
142
|
-
["to", "UPDATE_APPLIED"],
|
|
143
|
-
]) {
|
|
144
|
-
const plan = await env.DB.prepare(`
|
|
145
|
-
EXPLAIN QUERY PLAN SELECT * FROM bundle_events
|
|
146
|
-
WHERE type = ? AND platform = 'ios' AND channel = 'production'
|
|
147
|
-
AND ${direction}_bundle_id = ? AND received_at_ms >= 100 AND received_at_ms < 200
|
|
148
|
-
ORDER BY received_at_ms DESC, id DESC LIMIT 101
|
|
149
|
-
`)
|
|
150
|
-
.bind(type, "00000000-0000-0000-0000-000000000001")
|
|
151
|
-
.all<{ detail: string }>();
|
|
152
|
-
expect(plan.results.map(({ detail }) => detail).join("\n")).toContain(
|
|
153
|
-
`bundle_events_${direction}_bundle_idx`,
|
|
154
|
-
);
|
|
155
|
-
expect(plan.results.map(({ detail }) => detail).join("\n")).not.toContain(
|
|
156
|
-
"TEMP B-TREE",
|
|
157
|
-
);
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
for (const size of [-1, Number.MAX_SAFE_INTEGER + 1, null]) {
|
|
161
|
-
await expect(
|
|
162
|
-
env.DB.prepare("UPDATE bundles SET archive_byte_size = ?")
|
|
163
|
-
.bind(size)
|
|
164
|
-
.run(),
|
|
165
|
-
).rejects.toThrow(/constraint failed/);
|
|
166
|
-
await expect(
|
|
167
|
-
env.DB.prepare("UPDATE bundle_patches SET byte_size = ?")
|
|
168
|
-
.bind(size)
|
|
169
|
-
.run(),
|
|
170
|
-
).rejects.toThrow(/constraint failed/);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
await expect(
|
|
174
|
-
env.DB.prepare(`
|
|
175
|
-
INSERT INTO bundles (id, platform, file_hash, storage_uri)
|
|
176
|
-
VALUES ('missing-size', 'ios', 'hash', 'storage://bundle')
|
|
177
|
-
`).run(),
|
|
178
|
-
).rejects.toThrow(/NOT NULL constraint failed/);
|
|
179
|
-
await expect(
|
|
180
|
-
env.DB.prepare(`
|
|
181
|
-
INSERT INTO bundle_patches (
|
|
182
|
-
id, bundle_id, base_bundle_id, base_file_hash, patch_file_hash,
|
|
183
|
-
patch_storage_uri
|
|
184
|
-
) VALUES (
|
|
185
|
-
'missing-size', '00000000-0000-0000-0000-000000000001',
|
|
186
|
-
'00000000-0000-0000-0000-000000000001', 'base-hash', 'patch-hash',
|
|
187
|
-
'storage://patch'
|
|
188
|
-
)
|
|
189
|
-
`).run(),
|
|
190
|
-
).rejects.toThrow(/NOT NULL constraint failed/);
|
|
191
|
-
});
|
|
@@ -1,326 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
BundleRow,
|
|
3
|
-
ChannelRow,
|
|
4
|
-
ReleaseRow,
|
|
5
|
-
} from "@hot-updater/plugin-core";
|
|
6
|
-
import { toInsightsInstallationRow } from "@hot-updater/plugin-core";
|
|
7
|
-
import { setupDatabasePluginTestSuite } from "@hot-updater/test-utils";
|
|
8
|
-
import { env } from "cloudflare:test";
|
|
9
|
-
import {
|
|
10
|
-
afterAll,
|
|
11
|
-
beforeAll,
|
|
12
|
-
beforeEach,
|
|
13
|
-
describe,
|
|
14
|
-
expect,
|
|
15
|
-
inject,
|
|
16
|
-
it,
|
|
17
|
-
vi,
|
|
18
|
-
} from "vitest";
|
|
19
|
-
|
|
20
|
-
import { createBundleEventRowFixture } from "../../../../packages/test-utils/src/databaseTestFixtures";
|
|
21
|
-
import { d1Database } from "../../src/d1Database";
|
|
22
|
-
import { d1Database as d1RuntimeDatabase } from "../../src/worker";
|
|
23
|
-
|
|
24
|
-
const state = vi.hoisted<{ db: D1Database | undefined }>(() => ({
|
|
25
|
-
db: undefined,
|
|
26
|
-
}));
|
|
27
|
-
|
|
28
|
-
class D1TestStateError extends Error {
|
|
29
|
-
readonly name = "D1TestStateError";
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const getDb = (): D1Database => {
|
|
33
|
-
if (state.db === undefined) {
|
|
34
|
-
throw new D1TestStateError();
|
|
35
|
-
}
|
|
36
|
-
return state.db;
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
vi.mock("cloudflare", () => ({
|
|
40
|
-
default: class MockCloudflare {
|
|
41
|
-
readonly d1 = {
|
|
42
|
-
database: {
|
|
43
|
-
query: async (
|
|
44
|
-
_databaseId: string,
|
|
45
|
-
input:
|
|
46
|
-
| { readonly sql: string; readonly params?: readonly string[] }
|
|
47
|
-
| {
|
|
48
|
-
readonly batch: readonly {
|
|
49
|
-
readonly sql: string;
|
|
50
|
-
readonly params?: readonly string[];
|
|
51
|
-
}[];
|
|
52
|
-
},
|
|
53
|
-
) => {
|
|
54
|
-
const statements =
|
|
55
|
-
"batch" in input
|
|
56
|
-
? input.batch.map(({ sql, params }) =>
|
|
57
|
-
getDb()
|
|
58
|
-
.prepare(sql)
|
|
59
|
-
.bind(...(params ?? [])),
|
|
60
|
-
)
|
|
61
|
-
: (() => {
|
|
62
|
-
const params = input.params ?? [];
|
|
63
|
-
let paramOffset = 0;
|
|
64
|
-
return input.sql
|
|
65
|
-
.split(";")
|
|
66
|
-
.map((sql) => sql.trim())
|
|
67
|
-
.filter(Boolean)
|
|
68
|
-
.map((sql) => {
|
|
69
|
-
const paramCount = sql.match(/\?/g)?.length ?? 0;
|
|
70
|
-
const statement = getDb()
|
|
71
|
-
.prepare(sql)
|
|
72
|
-
.bind(
|
|
73
|
-
...params.slice(
|
|
74
|
-
paramOffset,
|
|
75
|
-
paramOffset + paramCount,
|
|
76
|
-
),
|
|
77
|
-
);
|
|
78
|
-
paramOffset += paramCount;
|
|
79
|
-
return statement;
|
|
80
|
-
});
|
|
81
|
-
})();
|
|
82
|
-
const results = await getDb().batch(statements);
|
|
83
|
-
return {
|
|
84
|
-
async *iterPages() {
|
|
85
|
-
yield { result: results };
|
|
86
|
-
},
|
|
87
|
-
};
|
|
88
|
-
},
|
|
89
|
-
},
|
|
90
|
-
};
|
|
91
|
-
},
|
|
92
|
-
}));
|
|
93
|
-
|
|
94
|
-
const reset = async (): Promise<void> => {
|
|
95
|
-
await getDb()
|
|
96
|
-
.prepare(
|
|
97
|
-
"DELETE FROM bundle_events; DELETE FROM bundle_installations; DELETE FROM api_keys; DELETE FROM bundle_patches; DELETE FROM release_catalogs; DELETE FROM releases; DELETE FROM bundles; DELETE FROM channels;",
|
|
98
|
-
)
|
|
99
|
-
.run();
|
|
100
|
-
};
|
|
101
|
-
|
|
102
|
-
const createChannelRow = (name: string): ChannelRow => ({
|
|
103
|
-
id: `channel-${name}`,
|
|
104
|
-
name,
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
const createBundleRow = (): BundleRow => ({
|
|
108
|
-
id: "00000000-0000-0000-0000-000000000902",
|
|
109
|
-
platform: "ios",
|
|
110
|
-
file_hash: "hash",
|
|
111
|
-
git_commit_hash: null,
|
|
112
|
-
storage_uri: "storage://bundle",
|
|
113
|
-
archive_byte_size: 3_000_000_001,
|
|
114
|
-
metadata: {},
|
|
115
|
-
manifest_storage_uri: null,
|
|
116
|
-
manifest_file_hash: null,
|
|
117
|
-
asset_base_storage_uri: null,
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
const createReleaseRow = (
|
|
121
|
-
channel: ChannelRow,
|
|
122
|
-
bundle: BundleRow,
|
|
123
|
-
): ReleaseRow => ({
|
|
124
|
-
id: "00000000-0000-7000-8000-000000000903",
|
|
125
|
-
revision: 1,
|
|
126
|
-
scope_key: `v1:test:${channel.name}:ios:app-version`,
|
|
127
|
-
channel_id: channel.id,
|
|
128
|
-
platform: bundle.platform,
|
|
129
|
-
kind: "BUNDLE",
|
|
130
|
-
bundle_id: bundle.id,
|
|
131
|
-
strategy: "APP_VERSION",
|
|
132
|
-
target_app_version: "1.0.0",
|
|
133
|
-
fingerprint_hash: null,
|
|
134
|
-
enabled: true,
|
|
135
|
-
should_force_update: false,
|
|
136
|
-
message: null,
|
|
137
|
-
rollout_cohort_count: 1000,
|
|
138
|
-
target_cohorts: [],
|
|
139
|
-
operation: "DEPLOY",
|
|
140
|
-
source_release_id: null,
|
|
141
|
-
created_at_ms: 100,
|
|
142
|
-
updated_at_ms: 100,
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
setupDatabasePluginTestSuite({
|
|
146
|
-
name: "cloudflare d1 http fixed-model database plugin",
|
|
147
|
-
migrate: async () => {
|
|
148
|
-
state.db = env.DB;
|
|
149
|
-
await getDb().prepare(inject("prepareSql")).run();
|
|
150
|
-
},
|
|
151
|
-
createPlugin: () =>
|
|
152
|
-
d1Database({
|
|
153
|
-
accountId: "account-id",
|
|
154
|
-
cloudflareApiToken: "api-token",
|
|
155
|
-
databaseId: "database-id",
|
|
156
|
-
}),
|
|
157
|
-
reset,
|
|
158
|
-
dispose: () => undefined,
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
setupDatabasePluginTestSuite({
|
|
162
|
-
name: "cloudflare worker d1 fixed-model database plugin",
|
|
163
|
-
migrate: () => undefined,
|
|
164
|
-
createPlugin: () => d1RuntimeDatabase(env.DB),
|
|
165
|
-
reset,
|
|
166
|
-
dispose: () => {
|
|
167
|
-
state.db = undefined;
|
|
168
|
-
},
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
describe.each([
|
|
172
|
-
{
|
|
173
|
-
name: "cloudflare d1 http",
|
|
174
|
-
createPlugin: () =>
|
|
175
|
-
d1Database({
|
|
176
|
-
accountId: "account-id",
|
|
177
|
-
cloudflareApiToken: "api-token",
|
|
178
|
-
databaseId: "database-id",
|
|
179
|
-
}),
|
|
180
|
-
},
|
|
181
|
-
{
|
|
182
|
-
name: "cloudflare worker d1",
|
|
183
|
-
createPlugin: () => d1RuntimeDatabase(env.DB),
|
|
184
|
-
},
|
|
185
|
-
])("$name Channel deletion", ({ createPlugin }) => {
|
|
186
|
-
beforeAll(() => {
|
|
187
|
-
state.db = env.DB;
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
beforeEach(async () => {
|
|
191
|
-
await reset();
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
afterAll(() => {
|
|
195
|
-
state.db = undefined;
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
it("deletes only empty channels", async () => {
|
|
199
|
-
const plugin = createPlugin();
|
|
200
|
-
const channel = createChannelRow("empty");
|
|
201
|
-
await plugin.models.channels.insert({
|
|
202
|
-
row: channel,
|
|
203
|
-
onConflict: "returnExisting",
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
await expect(
|
|
207
|
-
plugin.models.channels.delete({ id: channel.id }),
|
|
208
|
-
).resolves.toEqual({ deleted: true });
|
|
209
|
-
await expect(
|
|
210
|
-
plugin.models.channels.delete({ id: channel.id }),
|
|
211
|
-
).resolves.toEqual({ deleted: false, reason: "not_found" });
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
it("rolls back the snapshot when the later event insert fails, then safely retries", async () => {
|
|
215
|
-
const plugin = createPlugin();
|
|
216
|
-
const event = createBundleEventRowFixture("9101", 100);
|
|
217
|
-
const input = { event, installation: toInsightsInstallationRow(event) };
|
|
218
|
-
await env.DB.prepare(`
|
|
219
|
-
CREATE TRIGGER fail_insights_event BEFORE INSERT ON bundle_events
|
|
220
|
-
BEGIN SELECT RAISE(ABORT, 'injected event failure'); END;
|
|
221
|
-
`).run();
|
|
222
|
-
try {
|
|
223
|
-
await expect(plugin.models.insights.record(input)).rejects.toThrow(
|
|
224
|
-
"injected event failure",
|
|
225
|
-
);
|
|
226
|
-
expect(
|
|
227
|
-
(await env.DB.prepare("SELECT * FROM bundle_events").all()).results,
|
|
228
|
-
).toEqual([]);
|
|
229
|
-
expect(
|
|
230
|
-
(await env.DB.prepare("SELECT * FROM bundle_installations").all())
|
|
231
|
-
.results,
|
|
232
|
-
).toEqual([]);
|
|
233
|
-
} finally {
|
|
234
|
-
await env.DB.prepare("DROP TRIGGER fail_insights_event").run();
|
|
235
|
-
}
|
|
236
|
-
await plugin.models.insights.record(input);
|
|
237
|
-
await plugin.models.insights.record(input);
|
|
238
|
-
await expect(
|
|
239
|
-
plugin.models.insights.findInstallations({ installId: event.install_id }),
|
|
240
|
-
).resolves.toEqual([input.installation]);
|
|
241
|
-
expect(
|
|
242
|
-
(await env.DB.prepare("SELECT id FROM bundle_events").all()).results,
|
|
243
|
-
).toEqual([{ id: event.id }]);
|
|
244
|
-
});
|
|
245
|
-
|
|
246
|
-
it("rejects direct and committed deletion while a Release references the channel", async () => {
|
|
247
|
-
const plugin = createPlugin();
|
|
248
|
-
const channel = createChannelRow("active");
|
|
249
|
-
const bundle = createBundleRow();
|
|
250
|
-
const release = createReleaseRow(channel, bundle);
|
|
251
|
-
await plugin.models.channels.insert({
|
|
252
|
-
row: channel,
|
|
253
|
-
onConflict: "returnExisting",
|
|
254
|
-
});
|
|
255
|
-
await plugin.commit({
|
|
256
|
-
changes: [
|
|
257
|
-
{ model: "bundles", operation: "insert", row: bundle },
|
|
258
|
-
{ model: "releases", operation: "insert", row: release },
|
|
259
|
-
],
|
|
260
|
-
});
|
|
261
|
-
|
|
262
|
-
await expect(
|
|
263
|
-
plugin.models.channels.delete({ id: channel.id }),
|
|
264
|
-
).resolves.toEqual({ deleted: false, reason: "not_empty" });
|
|
265
|
-
await expect(
|
|
266
|
-
plugin.commit({
|
|
267
|
-
changes: [
|
|
268
|
-
{
|
|
269
|
-
model: "channels",
|
|
270
|
-
operation: "delete",
|
|
271
|
-
where: { id: channel.id },
|
|
272
|
-
},
|
|
273
|
-
],
|
|
274
|
-
}),
|
|
275
|
-
).resolves.toEqual({
|
|
276
|
-
committed: false,
|
|
277
|
-
conflict: { changeIndex: 0, reason: "referenced" },
|
|
278
|
-
});
|
|
279
|
-
await expect(plugin.models.bundles.findById(bundle.id)).resolves.toEqual(
|
|
280
|
-
bundle,
|
|
281
|
-
);
|
|
282
|
-
});
|
|
283
|
-
|
|
284
|
-
it("atomically deletes a Release before deleting its newly empty channel", async () => {
|
|
285
|
-
const plugin = createPlugin();
|
|
286
|
-
const channel = createChannelRow("retired");
|
|
287
|
-
const bundle = createBundleRow();
|
|
288
|
-
const release = createReleaseRow(channel, bundle);
|
|
289
|
-
await plugin.models.channels.insert({
|
|
290
|
-
row: channel,
|
|
291
|
-
onConflict: "returnExisting",
|
|
292
|
-
});
|
|
293
|
-
await plugin.commit({
|
|
294
|
-
changes: [
|
|
295
|
-
{ model: "bundles", operation: "insert", row: bundle },
|
|
296
|
-
{ model: "releases", operation: "insert", row: release },
|
|
297
|
-
],
|
|
298
|
-
});
|
|
299
|
-
|
|
300
|
-
await expect(
|
|
301
|
-
plugin.commit({
|
|
302
|
-
changes: [
|
|
303
|
-
{
|
|
304
|
-
model: "releases",
|
|
305
|
-
operation: "delete",
|
|
306
|
-
where: { id: release.id },
|
|
307
|
-
},
|
|
308
|
-
{
|
|
309
|
-
model: "channels",
|
|
310
|
-
operation: "delete",
|
|
311
|
-
where: { id: channel.id },
|
|
312
|
-
},
|
|
313
|
-
],
|
|
314
|
-
}),
|
|
315
|
-
).resolves.toEqual({ committed: true });
|
|
316
|
-
await expect(
|
|
317
|
-
plugin.models.releases.findById(release.id),
|
|
318
|
-
).resolves.toBeNull();
|
|
319
|
-
await expect(plugin.models.bundles.findById(bundle.id)).resolves.toEqual(
|
|
320
|
-
bundle,
|
|
321
|
-
);
|
|
322
|
-
await expect(plugin.models.channels.list({})).resolves.toEqual({
|
|
323
|
-
channels: [],
|
|
324
|
-
});
|
|
325
|
-
});
|
|
326
|
-
});
|