@hot-updater/cloudflare 1.0.0-rc.2 → 1.0.0-rc.21

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.
Files changed (51) hide show
  1. package/dist/index.cjs +95 -870
  2. package/dist/index.d.cts +724 -6
  3. package/dist/index.d.mts +724 -6
  4. package/dist/index.mjs +93 -870
  5. package/dist/init/index.cjs +11768 -0
  6. package/dist/init/index.d.cts +5 -1
  7. package/dist/init/index.d.mts +5 -1
  8. package/dist/init/index.mjs +11761 -1
  9. package/dist/worker/index.cjs +59 -859
  10. package/dist/worker/index.d.cts +725 -11
  11. package/dist/worker/index.d.mts +725 -11
  12. package/dist/worker/index.mjs +62 -863
  13. package/package.json +9 -13
  14. package/sql/bundles.sql +109 -190
  15. package/src/d1Database.ts +61 -40
  16. package/src/d1Executor.ts +84 -0
  17. package/src/d1Schema.ts +26 -0
  18. package/src/index.ts +1 -0
  19. package/src/plugins.ts +8 -0
  20. package/src/r2S3Storage.ts +4 -4
  21. package/src/r2Storage.ts +2 -2
  22. package/src/r2WorkerStorage.ts +4 -4
  23. package/src/worker/d1Database.ts +21 -27
  24. package/src/worker/index.ts +1 -0
  25. package/worker/dist/README.md +1 -1
  26. package/worker/dist/index.js +9682 -6206
  27. package/worker/dist/index.js.map +4 -4
  28. package/worker/migrations/0001_hot-updater_1.0.0.sql +109 -205
  29. package/worker/src/index.ts +2 -2
  30. package/dist/iac/index.cjs +0 -12566
  31. package/dist/iac/index.d.cts +0 -9
  32. package/dist/iac/index.d.mts +0 -9
  33. package/dist/iac/index.mjs +0 -12558
  34. package/src/d1Database.spec.ts +0 -178
  35. package/src/d1Implementation.spec.ts +0 -252
  36. package/src/d1Implementation.ts +0 -742
  37. package/src/d1Query.spec.ts +0 -46
  38. package/src/d1Query.ts +0 -89
  39. package/src/d1Rows.spec.ts +0 -111
  40. package/src/d1Rows.ts +0 -326
  41. package/src/d1Sql.spec.ts +0 -30
  42. package/src/d1Sql.ts +0 -175
  43. package/src/r2Storage.spec.ts +0 -346
  44. package/src/r2WorkerStorage.spec.ts +0 -184
  45. package/src/utils/createWrangler.spec.ts +0 -75
  46. package/src/worker/d1Database.spec.ts +0 -33
  47. package/worker/src/databaseMigration.integration.spec.ts +0 -130
  48. package/worker/src/databasePlugins.integration.spec.ts +0 -292
  49. package/worker/src/index.integration.spec.ts +0 -190
  50. package/worker/src/storagePlugin.integration.spec.ts +0 -59
  51. package/worker/src/testWorker.ts +0 -5
@@ -1,742 +0,0 @@
1
- import type {
2
- BundlePatchRow,
3
- BundleRow,
4
- ChannelInsertInput,
5
- ChannelInsertResult,
6
- ChannelRow,
7
- ChannelDeleteInput,
8
- ChannelDeleteResult,
9
- DatabaseChange,
10
- DatabaseCommit,
11
- DatabaseCommitExpectation,
12
- DatabaseCommitResult,
13
- } from "@hot-updater/plugin-core";
14
- import type {
15
- CreateDatabaseImplementationInput,
16
- DatabasePluginImplementation,
17
- DeleteDatabaseImplementationInput,
18
- FindOneDatabaseImplementationInput,
19
- UpdateDatabaseImplementationInput,
20
- } from "@hot-updater/plugin-core/internal";
21
-
22
- import { countD1Rows, d1TableNames, findManyD1Rows } from "./d1Query";
23
- import { parseD1Row } from "./d1Rows";
24
- import { buildD1Where, d1Placeholders, encodeD1Values } from "./d1Sql";
25
-
26
- export interface D1Executor {
27
- query(sql: string, params: readonly string[]): Promise<readonly unknown[]>;
28
- batch(
29
- statements: readonly D1Statement[],
30
- ): Promise<readonly (readonly unknown[])[]>;
31
- }
32
-
33
- export interface D1Statement {
34
- readonly sql: string;
35
- readonly params: readonly string[];
36
- }
37
-
38
- type D1Guard = {
39
- readonly sql: string;
40
- readonly params: readonly string[];
41
- };
42
-
43
- const EXPECTATION_CONFLICT_MARKER = "HOT_UPDATER_COMMIT_EXPECTATION_CONFLICT";
44
-
45
- const expectationGuard = (
46
- expectations: readonly DatabaseCommitExpectation[],
47
- ): D1Guard => {
48
- const clauses: string[] = [];
49
- const params: string[] = [];
50
- for (const expectation of expectations) {
51
- const isRelease = expectation.model === "releases";
52
- const table = isRelease ? "releases" : "release_catalogs";
53
- const keyField = isRelease ? "id" : "scope_key";
54
- const versionField = isRelease ? "revision" : "generation";
55
- const key = isRelease ? expectation.id : expectation.scopeKey;
56
- const version = isRelease ? expectation.revision : expectation.generation;
57
- if (version === null) {
58
- clauses.push(
59
- `NOT EXISTS (SELECT 1 FROM ${table} WHERE ${keyField} = json_extract(?, '$'))`,
60
- );
61
- params.push(...encodeD1Values([key]));
62
- } else {
63
- clauses.push(
64
- `EXISTS (SELECT 1 FROM ${table} WHERE ${keyField} = json_extract(?, '$') AND ${versionField} = json_extract(?, '$'))`,
65
- );
66
- params.push(...encodeD1Values([key, version]));
67
- }
68
- }
69
- return { sql: clauses.join(" AND ") || "1", params };
70
- };
71
-
72
- const readVersion = (
73
- row: unknown,
74
- field: "generation" | "revision",
75
- ): number | null => {
76
- if (typeof row !== "object" || row === null) return null;
77
- const value = Reflect.get(row, field);
78
- return typeof value === "number" ? value : null;
79
- };
80
-
81
- const expectationConflict = async (
82
- executor: D1Executor,
83
- expectations: readonly DatabaseCommitExpectation[],
84
- ): Promise<DatabaseCommitResult | null> => {
85
- for (const expectation of expectations) {
86
- const isRelease = expectation.model === "releases";
87
- const table = isRelease ? "releases" : "release_catalogs";
88
- const keyField = isRelease ? "id" : "scope_key";
89
- const versionField = isRelease ? "revision" : "generation";
90
- const key = isRelease ? expectation.id : expectation.scopeKey;
91
- const expectedVersion = isRelease
92
- ? expectation.revision
93
- : expectation.generation;
94
- const rows = await executor.query(
95
- `SELECT ${versionField} FROM ${table} WHERE ${keyField} = json_extract(?, '$') LIMIT 1`,
96
- encodeD1Values([key]),
97
- );
98
- const actualVersion = readVersion(rows[0], versionField);
99
- if (actualVersion !== expectedVersion) {
100
- return {
101
- committed: false,
102
- conflict: {
103
- actualVersion,
104
- changeIndex: -1,
105
- expectedVersion,
106
- key,
107
- model: expectation.model,
108
- reason: "version_conflict",
109
- },
110
- };
111
- }
112
- }
113
- return null;
114
- };
115
-
116
- const isExpectationConflictError = (error: unknown): boolean => {
117
- const message = error instanceof Error ? error.message : String(error);
118
- return (
119
- message.includes(EXPECTATION_CONFLICT_MARKER) ||
120
- message.toLowerCase().includes("malformed json")
121
- );
122
- };
123
-
124
- const bundleValues = (row: BundleRow): readonly unknown[] => [
125
- row.id,
126
- row.platform,
127
- row.file_hash,
128
- row.git_commit_hash,
129
- row.storage_uri,
130
- row.archive_byte_size,
131
- row.metadata,
132
- row.manifest_storage_uri,
133
- row.manifest_file_hash,
134
- row.asset_base_storage_uri,
135
- ];
136
-
137
- const patchValues = (row: BundlePatchRow): readonly unknown[] => [
138
- row.id,
139
- row.bundle_id,
140
- row.base_bundle_id,
141
- row.base_file_hash,
142
- row.patch_file_hash,
143
- row.patch_storage_uri,
144
- row.byte_size,
145
- row.order_index,
146
- ];
147
-
148
- const channelValues = (row: ChannelRow): readonly unknown[] => [
149
- row.id,
150
- row.name,
151
- ];
152
-
153
- const insertQuery = (
154
- input: CreateDatabaseImplementationInput,
155
- guard?: D1Guard,
156
- conflictMode: "returnExisting" | "ignore" = "returnExisting",
157
- ): D1Statement => {
158
- let columns: readonly string[];
159
- let values: readonly unknown[];
160
- switch (input.model) {
161
- case "bundles":
162
- columns = [
163
- "id",
164
- "platform",
165
- "file_hash",
166
- "git_commit_hash",
167
- "storage_uri",
168
- "archive_byte_size",
169
- "metadata",
170
- "manifest_storage_uri",
171
- "manifest_file_hash",
172
- "asset_base_storage_uri",
173
- ];
174
- values = bundleValues(input.data);
175
- break;
176
- case "bundle_patches":
177
- columns = [
178
- "id",
179
- "bundle_id",
180
- "base_bundle_id",
181
- "base_file_hash",
182
- "patch_file_hash",
183
- "patch_storage_uri",
184
- "byte_size",
185
- "order_index",
186
- ];
187
- values = patchValues(input.data);
188
- break;
189
- case "channels":
190
- columns = ["id", "name"];
191
- values = channelValues(input.data);
192
- break;
193
- case "bundle_events":
194
- case "api_keys":
195
- case "release_catalogs":
196
- case "releases":
197
- columns = Object.keys(input.data);
198
- values = Object.values(input.data);
199
- break;
200
- }
201
-
202
- const conflict =
203
- input.onConflict !== "ignore"
204
- ? ""
205
- : input.model === "channels"
206
- ? conflictMode === "ignore"
207
- ? " ON CONFLICT(name) DO NOTHING"
208
- : " ON CONFLICT(name) DO UPDATE SET name = excluded.name"
209
- : input.model === "api_keys"
210
- ? conflictMode === "ignore"
211
- ? " ON CONFLICT(hash) DO NOTHING"
212
- : " ON CONFLICT(hash) DO UPDATE SET hash = excluded.hash"
213
- : "";
214
-
215
- return {
216
- sql: `INSERT INTO ${d1TableNames[input.model]} (${columns.join(", ")}) ${
217
- guard === undefined
218
- ? `VALUES (${d1Placeholders(values.length)})`
219
- : `SELECT ${d1Placeholders(values.length)} WHERE ${guard.sql}`
220
- }${conflict} RETURNING *`,
221
- params: [...encodeD1Values(values), ...(guard?.params ?? [])],
222
- };
223
- };
224
-
225
- const updateEntries = (
226
- update: UpdateDatabaseImplementationInput["update"],
227
- ): readonly [string, unknown][] => Object.entries(update);
228
-
229
- const updateQuery = (
230
- input: UpdateDatabaseImplementationInput,
231
- guard?: D1Guard,
232
- ): D1Statement => {
233
- const entries = updateEntries(input.update);
234
- const where = buildD1Where(input.where);
235
- if (entries.length === 0) {
236
- return {
237
- sql: `SELECT * FROM ${d1TableNames[input.model]}${where.sql}${guard ? ` AND ${guard.sql}` : ""} LIMIT 1`,
238
- params: [...where.params, ...(guard?.params ?? [])],
239
- };
240
- }
241
- const assignments = entries
242
- .map(([field]) => `${field} = json_extract(?, '$')`)
243
- .join(", ");
244
- return {
245
- sql: `UPDATE ${d1TableNames[input.model]} SET ${assignments}${where.sql}${guard ? ` AND ${guard.sql}` : ""} RETURNING *`,
246
- params: [
247
- ...encodeD1Values(entries.map(([, value]) => value)),
248
- ...where.params,
249
- ...(guard?.params ?? []),
250
- ],
251
- };
252
- };
253
-
254
- const deleteQuery = (
255
- input: DeleteDatabaseImplementationInput,
256
- guard?: D1Guard,
257
- ): D1Statement => {
258
- const where = buildD1Where(input.where);
259
- return {
260
- sql: `DELETE FROM ${d1TableNames[input.model]}${where.sql}${guard ? ` AND ${guard.sql}` : ""}`,
261
- params: [...where.params, ...(guard?.params ?? [])],
262
- };
263
- };
264
-
265
- type D1RequiredRow = {
266
- readonly model: "bundles" | "api_keys" | "releases";
267
- readonly id: string;
268
- readonly changeIndex: number;
269
- };
270
-
271
- type D1Check = {
272
- readonly changeIndex: number;
273
- readonly resultIndex: number;
274
- readonly conflictWhen: "empty" | "nonempty";
275
- readonly reason: "not_found" | "referenced";
276
- };
277
-
278
- type D1CommitPlan = {
279
- readonly checks: readonly D1Check[];
280
- readonly statements: readonly D1Statement[];
281
- };
282
-
283
- const insertedKey = (change: DatabaseChange): string | undefined => {
284
- if (change.operation !== "insert") return undefined;
285
- if (change.model === "bundles") return `bundles:${change.row.id}`;
286
- if (change.model === "apiKeys") {
287
- return `api_keys:${change.row.id}`;
288
- }
289
- if (change.model === "releases") {
290
- return `releases:${change.row.id}`;
291
- }
292
- return undefined;
293
- };
294
-
295
- const requiredRow = (
296
- change: DatabaseChange,
297
- changeIndex: number,
298
- ): D1RequiredRow | undefined => {
299
- if (change.operation !== "update") return undefined;
300
- if (change.model === "bundles") {
301
- return {
302
- model: "bundles",
303
- id: change.where.id,
304
- changeIndex,
305
- };
306
- }
307
- if (change.model === "apiKeys") {
308
- return {
309
- model: "api_keys",
310
- id: change.where.id,
311
- changeIndex,
312
- };
313
- }
314
- if (change.model === "releases") {
315
- return {
316
- model: "releases",
317
- id: change.where.id,
318
- changeIndex,
319
- };
320
- }
321
- return undefined;
322
- };
323
-
324
- type D1ChannelDeletePrecondition = {
325
- readonly id: string;
326
- readonly excludedReleaseIds: readonly string[];
327
- readonly addedReferenceReleaseIds: readonly string[];
328
- };
329
-
330
- const channelDeletePrecondition = (
331
- changes: readonly DatabaseChange[],
332
- changeIndex: number,
333
- channelId: string,
334
- ): D1ChannelDeletePrecondition => {
335
- const releaseEffects = new Map<string, string | null>();
336
- for (const change of changes.slice(0, changeIndex)) {
337
- if (change.model !== "releases") continue;
338
- switch (change.operation) {
339
- case "insert":
340
- releaseEffects.set(change.row.id, change.row.channel_id);
341
- break;
342
- case "update":
343
- break;
344
- case "delete":
345
- releaseEffects.set(change.where.id, null);
346
- break;
347
- }
348
- }
349
- return {
350
- id: channelId,
351
- excludedReleaseIds: [...releaseEffects.keys()],
352
- addedReferenceReleaseIds: [...releaseEffects]
353
- .filter(([, finalChannelId]) => finalChannelId === channelId)
354
- .map(([releaseId]) => releaseId),
355
- };
356
- };
357
-
358
- const changeQuery = (change: DatabaseChange, guard: D1Guard): D1Statement => {
359
- switch (change.model) {
360
- case "bundles":
361
- switch (change.operation) {
362
- case "insert":
363
- return insertQuery({ model: "bundles", data: change.row }, guard);
364
- case "update":
365
- return updateQuery(
366
- {
367
- model: "bundles",
368
- where: [{ field: "id", value: change.where.id }],
369
- update: change.update,
370
- },
371
- guard,
372
- );
373
- case "delete":
374
- return deleteQuery(
375
- {
376
- model: "bundles",
377
- where: [{ field: "id", value: change.where.id }],
378
- },
379
- guard,
380
- );
381
- }
382
- case "bundlePatches":
383
- return change.operation === "insert"
384
- ? insertQuery({ model: "bundle_patches", data: change.row }, guard)
385
- : deleteQuery(
386
- {
387
- model: "bundle_patches",
388
- where: [{ field: "bundle_id", value: change.where.bundleId }],
389
- },
390
- guard,
391
- );
392
- case "releases":
393
- switch (change.operation) {
394
- case "insert":
395
- return insertQuery({ model: "releases", data: change.row }, guard);
396
- case "update":
397
- return updateQuery(
398
- {
399
- model: "releases",
400
- where: [{ field: "id", value: change.where.id }],
401
- update: change.update,
402
- },
403
- guard,
404
- );
405
- case "delete":
406
- return deleteQuery(
407
- {
408
- model: "releases",
409
- where: [{ field: "id", value: change.where.id }],
410
- },
411
- guard,
412
- );
413
- }
414
- case "releaseCatalogs":
415
- return {
416
- sql: `INSERT INTO release_catalogs (${Object.keys(change.row).join(", ")}) SELECT ${d1Placeholders(Object.keys(change.row).length)} WHERE ${guard.sql} ON CONFLICT(scope_key) DO UPDATE SET ${Object.keys(
417
- change.row,
418
- )
419
- .filter((field) => field !== "scope_key")
420
- .map((field) => `${field} = excluded.${field}`)
421
- .join(", ")} RETURNING *`,
422
- params: [...encodeD1Values(Object.values(change.row)), ...guard.params],
423
- };
424
- case "channels":
425
- return change.operation === "insert"
426
- ? insertQuery(
427
- {
428
- model: "channels",
429
- data: change.row,
430
- onConflict: change.onConflict,
431
- },
432
- guard,
433
- "ignore",
434
- )
435
- : {
436
- sql: `DELETE FROM channels WHERE id = json_extract(?, '$') AND NOT EXISTS (SELECT 1 FROM releases WHERE channel_id = json_extract(?, '$')) AND ${guard.sql}`,
437
- params: [
438
- ...encodeD1Values([change.where.id, change.where.id]),
439
- ...guard.params,
440
- ],
441
- };
442
- case "insights":
443
- return insertQuery({ model: "bundle_events", data: change.row }, guard);
444
- case "apiKeys":
445
- return change.operation === "insert"
446
- ? insertQuery(
447
- {
448
- model: "api_keys",
449
- data: change.row,
450
- onConflict: change.onConflict,
451
- },
452
- guard,
453
- "ignore",
454
- )
455
- : updateQuery(
456
- {
457
- model: "api_keys",
458
- where: [{ field: "id", value: change.where.id }],
459
- update: { revoked_at_ms: change.update.revokedAtMs },
460
- },
461
- guard,
462
- );
463
- }
464
- };
465
-
466
- const createCommitPlan = (input: DatabaseCommit): D1CommitPlan => {
467
- const checks: D1Check[] = [];
468
- const statements: D1Statement[] = [];
469
- const expectations = input.expectations ?? [];
470
- if (expectations.length > 0) {
471
- const guard = expectationGuard(expectations);
472
- statements.push({
473
- sql: `SELECT CASE WHEN ${guard.sql} THEN 1 ELSE json_extract('${EXPECTATION_CONFLICT_MARKER}', '$') END AS expectation_guard`,
474
- params: guard.params,
475
- });
476
- }
477
- const requiredRows: D1RequiredRow[] = [];
478
- const channelDeletes: D1ChannelDeletePrecondition[] = [];
479
- const inserted = new Set<string>();
480
- for (const [changeIndex, change] of input.changes.entries()) {
481
- const required = requiredRow(change, changeIndex);
482
- if (required !== undefined) {
483
- if (!inserted.has(`${required.model}:${required.id}`)) {
484
- requiredRows.push(required);
485
- checks.push({
486
- changeIndex,
487
- resultIndex: statements.length,
488
- conflictWhen: "empty",
489
- reason: "not_found",
490
- });
491
- statements.push({
492
- sql: `SELECT id FROM ${d1TableNames[required.model]} WHERE id = json_extract(?, '$') LIMIT 1`,
493
- params: encodeD1Values([required.id]),
494
- });
495
- }
496
- }
497
- if (change.model === "channels" && change.operation === "delete") {
498
- const precondition = channelDeletePrecondition(
499
- input.changes,
500
- changeIndex,
501
- change.where.id,
502
- );
503
- channelDeletes.push(precondition);
504
- checks.push({
505
- changeIndex,
506
- resultIndex: statements.length,
507
- conflictWhen: "nonempty",
508
- reason: "referenced",
509
- });
510
- statements.push({
511
- sql: "SELECT id FROM releases WHERE channel_id = json_extract(?, '$') AND id NOT IN (SELECT value FROM json_each(?)) UNION ALL SELECT value AS id FROM json_each(?) LIMIT 1",
512
- params: encodeD1Values([
513
- precondition.id,
514
- precondition.excludedReleaseIds,
515
- precondition.addedReferenceReleaseIds,
516
- ]),
517
- });
518
- }
519
- const key = insertedKey(change);
520
- if (key !== undefined) inserted.add(key);
521
- }
522
-
523
- const guard: D1Guard = {
524
- sql: `NOT EXISTS (
525
- SELECT 1 FROM json_each(?) AS required
526
- WHERE NOT EXISTS (
527
- SELECT 1 FROM bundles
528
- WHERE json_extract(required.value, '$.model') = 'bundles'
529
- AND bundles.id = json_extract(required.value, '$.id')
530
- UNION ALL
531
- SELECT 1 FROM api_keys
532
- WHERE json_extract(required.value, '$.model') = 'api_keys'
533
- AND api_keys.id = json_extract(required.value, '$.id')
534
- UNION ALL
535
- SELECT 1 FROM channels
536
- WHERE json_extract(required.value, '$.model') = 'channels'
537
- AND channels.id = json_extract(required.value, '$.id')
538
- UNION ALL
539
- SELECT 1 FROM releases
540
- WHERE json_extract(required.value, '$.model') = 'releases'
541
- AND releases.id = json_extract(required.value, '$.id')
542
- )
543
- ) AND NOT EXISTS (
544
- SELECT 1 FROM json_each(?) AS channel_delete
545
- WHERE EXISTS (
546
- SELECT 1 FROM releases
547
- WHERE releases.channel_id = json_extract(channel_delete.value, '$.id')
548
- AND releases.id NOT IN (
549
- SELECT value FROM json_each(
550
- json_extract(channel_delete.value, '$.excludedReleaseIds')
551
- )
552
- )
553
- UNION ALL
554
- SELECT value FROM json_each(
555
- json_extract(channel_delete.value, '$.addedReferenceReleaseIds')
556
- )
557
- )
558
- )`,
559
- params: encodeD1Values([
560
- requiredRows.map(({ model, id }) => ({ model, id })),
561
- channelDeletes,
562
- ]),
563
- };
564
-
565
- statements.push(...input.changes.map((change) => changeQuery(change, guard)));
566
- return { checks, statements };
567
- };
568
-
569
- const resultForPlan = (
570
- plan: D1CommitPlan,
571
- results: readonly (readonly unknown[])[],
572
- ): DatabaseCommitResult => {
573
- const missing = plan.checks.find(({ conflictWhen, resultIndex }) =>
574
- conflictWhen === "empty"
575
- ? (results[resultIndex]?.length ?? 0) === 0
576
- : (results[resultIndex]?.length ?? 0) > 0,
577
- );
578
- return missing === undefined
579
- ? { committed: true }
580
- : {
581
- committed: false,
582
- conflict: {
583
- changeIndex: missing.changeIndex,
584
- reason: missing.reason,
585
- },
586
- };
587
- };
588
-
589
- const insertChannel = async (
590
- executor: D1Executor,
591
- input: ChannelInsertInput,
592
- ): Promise<ChannelInsertResult> => {
593
- const insert = insertQuery(
594
- { model: "channels", data: input.row, onConflict: "ignore" },
595
- undefined,
596
- "ignore",
597
- );
598
- const select = {
599
- sql: "SELECT id, name FROM channels WHERE name = json_extract(?, '$') LIMIT 1",
600
- params: encodeD1Values([input.row.name]),
601
- };
602
- const [insertedRows = [], canonicalRows = []] = await executor.batch([
603
- insert,
604
- select,
605
- ]);
606
- return {
607
- row: parseD1Row("channels", canonicalRows[0]),
608
- inserted: insertedRows.length > 0,
609
- };
610
- };
611
-
612
- const deleteChannel = async (
613
- executor: D1Executor,
614
- input: ChannelDeleteInput,
615
- ): Promise<ChannelDeleteResult> => {
616
- const [channelRows = [], referenceRows = [], deletedRows = []] =
617
- await executor.batch([
618
- {
619
- sql: "SELECT id FROM channels WHERE id = json_extract(?, '$') LIMIT 1",
620
- params: encodeD1Values([input.id]),
621
- },
622
- {
623
- sql: "SELECT id FROM releases WHERE channel_id = json_extract(?, '$') LIMIT 1",
624
- params: encodeD1Values([input.id]),
625
- },
626
- {
627
- sql: "DELETE FROM channels WHERE id = json_extract(?, '$') AND NOT EXISTS (SELECT 1 FROM releases WHERE channel_id = json_extract(?, '$')) RETURNING id",
628
- params: encodeD1Values([input.id, input.id]),
629
- },
630
- ]);
631
- if (channelRows.length === 0) return { deleted: false, reason: "not_found" };
632
- if (referenceRows.length > 0) return { deleted: false, reason: "not_empty" };
633
- return deletedRows.length > 0
634
- ? { deleted: true }
635
- : { deleted: false, reason: "not_empty" };
636
- };
637
-
638
- export const createD1Implementation = (
639
- executor: D1Executor,
640
- ): DatabasePluginImplementation => ({
641
- async create(input) {
642
- const query = insertQuery(input);
643
- const rows = await executor.query(query.sql, query.params);
644
- switch (input.model) {
645
- case "bundles":
646
- return parseD1Row("bundles", rows[0]);
647
- case "bundle_patches":
648
- return parseD1Row("bundle_patches", rows[0]);
649
- case "channels":
650
- return parseD1Row("channels", rows[0]);
651
- case "bundle_events":
652
- return parseD1Row("bundle_events", rows[0]);
653
- case "api_keys":
654
- return parseD1Row("api_keys", rows[0]);
655
- case "releases":
656
- return parseD1Row("releases", rows[0]);
657
- case "release_catalogs":
658
- return parseD1Row("release_catalogs", rows[0]);
659
- }
660
- },
661
- async update(input) {
662
- const query = updateQuery(input);
663
- const rows = await executor.query(query.sql, query.params);
664
- if (rows[0] === undefined) return null;
665
- switch (input.model) {
666
- case "bundles":
667
- return parseD1Row("bundles", rows[0]);
668
- case "api_keys":
669
- return parseD1Row("api_keys", rows[0]);
670
- case "releases":
671
- return parseD1Row("releases", rows[0]);
672
- case "release_catalogs":
673
- return parseD1Row("release_catalogs", rows[0]);
674
- }
675
- },
676
- async delete(input) {
677
- const query = deleteQuery(input);
678
- await executor.query(query.sql, query.params);
679
- },
680
- count: (input) => countD1Rows(executor, input),
681
- async findOne(input: FindOneDatabaseImplementationInput) {
682
- const where = buildD1Where(input.where);
683
- const rows = await executor.query(
684
- `SELECT * FROM ${d1TableNames[input.model]}${where.sql} LIMIT 1`,
685
- where.params,
686
- );
687
- if (rows[0] === undefined) return null;
688
- switch (input.model) {
689
- case "bundles":
690
- return parseD1Row("bundles", rows[0]);
691
- case "bundle_patches":
692
- return parseD1Row("bundle_patches", rows[0]);
693
- case "channels":
694
- return parseD1Row("channels", rows[0]);
695
- case "api_keys":
696
- return parseD1Row("api_keys", rows[0]);
697
- case "releases":
698
- return parseD1Row("releases", rows[0]);
699
- case "release_catalogs":
700
- return parseD1Row("release_catalogs", rows[0]);
701
- }
702
- },
703
- findMany: (input) => findManyD1Rows(executor, input),
704
- insertChannel: (input) => insertChannel(executor, input),
705
- deleteChannel: (input) => deleteChannel(executor, input),
706
- async commit(input) {
707
- if (input.changes.length === 0) return { committed: true };
708
- const expectations = input.expectations ?? [];
709
- const conflict = await expectationConflict(executor, expectations);
710
- if (conflict !== null) return conflict;
711
- const plan = createCommitPlan(input);
712
- try {
713
- return resultForPlan(plan, await executor.batch(plan.statements));
714
- } catch (error) {
715
- if (expectations.length === 0 || !isExpectationConflictError(error)) {
716
- throw error;
717
- }
718
- return (
719
- (await expectationConflict(executor, expectations)) ?? {
720
- committed: false,
721
- conflict: {
722
- actualVersion:
723
- expectations[0].model === "releases"
724
- ? expectations[0].revision
725
- : expectations[0].generation,
726
- changeIndex: -1,
727
- expectedVersion:
728
- expectations[0].model === "releases"
729
- ? expectations[0].revision
730
- : expectations[0].generation,
731
- key:
732
- expectations[0].model === "releases"
733
- ? expectations[0].id
734
- : expectations[0].scopeKey,
735
- model: expectations[0].model,
736
- reason: "version_conflict",
737
- },
738
- }
739
- );
740
- }
741
- },
742
- });