@hot-updater/cloudflare 1.1.3 → 1.2.0-rc.0

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/src/d1Database.ts CHANGED
@@ -1,86 +1,46 @@
1
- import {
2
- coreSettings,
3
- coreTarget,
4
- type SqlStatement,
5
- } from "@hot-updater/plugin-core";
1
+ import { coreSettings, coreTarget } from "@hot-updater/plugin-core";
6
2
  import type {
7
3
  SchemaGenerator,
8
4
  ToolingDatabase,
9
5
  } from "@hot-updater/plugin-core";
10
- import Cloudflare from "cloudflare";
11
6
 
12
- import {
13
- createD1Database,
14
- D1ExecutionError,
15
- type D1ResultLike,
16
- toSqlResult,
17
- } from "./d1Executor";
7
+ import { createD1Database } from "./d1Executor";
8
+ import { type D1RestConfig, d1RestRunner } from "./d1Rest";
18
9
  import { d1SchemaSql } from "./d1Schema";
19
10
 
20
11
  export { D1ExecutionError } from "./d1Executor";
21
12
 
22
- export interface D1DatabaseConfig {
23
- readonly databaseId: string;
24
- readonly accountId: string;
25
- readonly cloudflareApiToken: string;
26
- }
13
+ export type D1DatabaseConfig = D1RestConfig;
27
14
 
28
- /** The REST API binds text: each value goes as JSON and is read back with `json_extract`. */
29
- const encode = ({ sql, params }: SqlStatement) => ({
30
- // A function, since `$'` in a replacement string means "the rest of the input".
31
- sql: sql.replaceAll("?", () => "json_extract(?, '$')"),
32
- params: params.map((value) => JSON.stringify(value ?? null)),
33
- });
15
+ /**
16
+ * The resource this database reaches, without credentials, under the key
17
+ * `hot-updater doctor` reads to tell the database `hot-updater init` saved
18
+ * from another (a convention CLAUDE.md documents).
19
+ */
20
+ const DATABASE_TARGET = Symbol.for("@hot-updater/database-target");
21
+
22
+ /** `target` under {@link DATABASE_TARGET}, to spread into the database. */
23
+ const marked = (target: string) => ({ [DATABASE_TARGET]: target });
34
24
 
35
25
  /**
36
26
  * Hot Updater's database on D1 through the Cloudflare REST API, for the CLI
37
27
  * and console. `hot-updater db generate` writes a D1 migration of the built-in
38
28
  * tables and the server's plugin tables to `migrations`, and
39
- * `hot-updater db migrate` applies them through the API.
29
+ * `hot-updater db migrate` applies them through the API. The managed
30
+ * database migrates with the Worker's migrations when `hot-updater init` runs
31
+ * again.
40
32
  */
41
- export const d1Database = (config: D1DatabaseConfig): ToolingDatabase => {
42
- const cloudflare = new Cloudflare({
43
- apiToken: config.cloudflareApiToken,
44
- });
45
- const execute = async (statements: readonly SqlStatement[]) => {
46
- const [first] = statements;
47
- if (first === undefined) return [];
48
- const body =
49
- statements.length === 1
50
- ? { account_id: config.accountId, ...encode(first) }
51
- : { account_id: config.accountId, batch: statements.map(encode) };
52
- // cloudflare@4 predates the D1 REST API's parameterized batch body type.
53
- const page = await cloudflare.d1.database.query(
54
- config.databaseId,
55
- body as unknown as Parameters<typeof cloudflare.d1.database.query>[1],
56
- );
57
- const results = [];
58
- for await (const resultPage of page.iterPages()) {
59
- for (const result of resultPage.result) {
60
- results.push(toSqlResult(result as D1ResultLike));
61
- }
33
+ export const d1Database = (config: D1DatabaseConfig): ToolingDatabase => ({
34
+ ...createD1Database(d1RestRunner(config)),
35
+ ...marked(`d1/${config.databaseId}`),
36
+ generateSchema: ((version, _name, target = coreTarget) => {
37
+ if (version !== "latest" && version !== coreSettings["schema.core"]) {
38
+ throw new Error(`Invalid version ${version}`);
62
39
  }
63
- if (results.length !== statements.length) throw new D1ExecutionError();
64
- return results;
65
- };
66
- return {
67
- ...createD1Database({
68
- query: async (statement) => (await execute([statement]))[0]!,
69
- batch: execute,
70
- }),
71
- generateSchema: ((version, _name, target = coreTarget) => {
72
- if (version !== "latest" && version !== coreSettings["schema.core"]) {
73
- throw new Error(`Invalid version ${version}`);
74
- }
75
- // Wrangler applies migrations in the order of their leading numbers.
76
- const timestamp = new Date()
77
- .toISOString()
78
- .replace(/\D/g, "")
79
- .slice(0, 14);
80
- return {
81
- code: d1SchemaSql(target),
82
- path: `migrations/${timestamp}_hot-updater.sql`,
83
- };
84
- }) satisfies SchemaGenerator,
85
- };
86
- };
40
+ const timestamp = new Date().toISOString().replace(/\D/g, "").slice(0, 14);
41
+ return {
42
+ code: d1SchemaSql(target),
43
+ path: `migrations/${timestamp}_hot-updater.sql`,
44
+ };
45
+ }) satisfies SchemaGenerator,
46
+ });
package/src/d1Rest.ts ADDED
@@ -0,0 +1,54 @@
1
+ import type { SqlResult, SqlStatement } from "@hot-updater/plugin-core";
2
+ import Cloudflare from "cloudflare";
3
+
4
+ import { D1ExecutionError, type D1ResultLike, toSqlResult } from "./d1Executor";
5
+
6
+ export interface D1RestConfig {
7
+ readonly databaseId: string;
8
+ readonly accountId: string;
9
+ readonly cloudflareApiToken: string;
10
+ }
11
+
12
+ /** The REST API binds text: each value goes as JSON and is read back with `json_extract`. */
13
+ const encode = ({ sql, params }: SqlStatement) => ({
14
+ // A function, since `$'` in a replacement string means "the rest of the input".
15
+ sql: sql.replaceAll("?", () => "json_extract(?, '$')"),
16
+ params: params.map((value) => JSON.stringify(value ?? null)),
17
+ });
18
+
19
+ /**
20
+ * Statements on a D1 database through the Cloudflare REST API: one at a
21
+ * time, or a batch D1 runs in one transaction.
22
+ */
23
+ export const d1RestRunner = (config: D1RestConfig) => {
24
+ const cloudflare = new Cloudflare({
25
+ apiToken: config.cloudflareApiToken,
26
+ });
27
+ const batch = async (
28
+ statements: readonly SqlStatement[],
29
+ ): Promise<SqlResult[]> => {
30
+ const [first] = statements;
31
+ if (first === undefined) return [];
32
+ const body =
33
+ statements.length === 1
34
+ ? { account_id: config.accountId, ...encode(first) }
35
+ : { account_id: config.accountId, batch: statements.map(encode) };
36
+ // cloudflare@4 predates the D1 REST API's parameterized batch body type.
37
+ const page = await cloudflare.d1.database.query(
38
+ config.databaseId,
39
+ body as unknown as Parameters<typeof cloudflare.d1.database.query>[1],
40
+ );
41
+ const results = [];
42
+ for await (const resultPage of page.iterPages()) {
43
+ for (const result of resultPage.result) {
44
+ results.push(toSqlResult(result as D1ResultLike));
45
+ }
46
+ }
47
+ if (results.length !== statements.length) throw new D1ExecutionError();
48
+ return results;
49
+ };
50
+ return {
51
+ query: async (statement: SqlStatement) => (await batch([statement]))[0]!,
52
+ batch,
53
+ };
54
+ };
package/src/d1Schema.ts CHANGED
@@ -19,8 +19,38 @@ export const d1SchemaStatements = ({
19
19
  ...generateEngineSql("sqlite", schema, settings),
20
20
  ];
21
21
 
22
+ const HEADER = "-- HotUpdater.schema\n\n";
23
+
24
+ const toSql = (statements: readonly string[]) =>
25
+ `${HEADER}${statements.map((statement) => `${statement};`).join("\n\n")}\n`;
26
+
22
27
  /** The schema as a D1 migration, which `wrangler d1 migrations apply` runs. */
23
28
  export const d1SchemaSql = (target?: ToolingTarget): string =>
24
- `-- HotUpdater.schema\n\n${d1SchemaStatements(target)
25
- .map((statement) => `${statement};`)
26
- .join("\n\n")}\n`;
29
+ toSql(d1SchemaStatements(target));
30
+
31
+ /** The statements of a migration `d1SchemaSql` or `d1MigrationSql` wrote. */
32
+ export const d1MigrationStatements = (sql: string): string[] =>
33
+ sql
34
+ .replace(HEADER, "")
35
+ .split(";\n\n")
36
+ .map((statement) => statement.replace(/;\n$/u, "").trim())
37
+ .filter((statement) => statement !== "");
38
+
39
+ /**
40
+ * A managed D1 migration after the first: each statement of the target's
41
+ * schema that no earlier migration ran, then every settings row, so the
42
+ * migration is never empty and the fence passes once it ran. Shipped
43
+ * migrations are frozen; a schema change goes into the next one.
44
+ */
45
+ export const d1MigrationSql = (
46
+ target: ToolingTarget,
47
+ earlier: readonly string[],
48
+ ): string => {
49
+ const statements = d1SchemaStatements(target);
50
+ const settings = Object.keys(target.settings).length;
51
+ const ran = new Set(earlier.flatMap(d1MigrationStatements));
52
+ return toSql([
53
+ ...statements.slice(0, -settings).filter((sql) => !ran.has(sql)),
54
+ ...statements.slice(-settings),
55
+ ]);
56
+ };
@@ -1 +1 @@
1
- This folder contains the built output assets for the worker "hot-updater" generated at 2026-10-26T01:16:59.468Z.
1
+ This folder contains the built output assets for the worker "hot-updater" generated at 2026-10-27T15:32:58.641Z.