@gscdump/cloudflare 1.5.0 → 1.5.3
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/server-tail/index.d.mts +3 -2
- package/dist/server-tail/index.mjs +3 -2
- package/dist/server-tail/r2-sql-client.d.mts +13 -1
- package/dist/server-tail/r2-sql-client.mjs +15 -50
- package/dist/server-tail/r2-sql-transport.d.mts +35 -0
- package/dist/server-tail/r2-sql-transport.mjs +132 -0
- package/package.json +5 -5
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ArchetypeSqlPlan, BuildArchetypeSqlOptions, TABLE_PLACEHOLDER, buildArchetypeSql } from "./archetype-sql.mjs";
|
|
2
2
|
import { DuckDbIcebergError, DuckDbIcebergExecutor, DuckDbIcebergExecutorConfig, DuckDbIcebergQueryError, DuckDbIcebergResult, DuckDbIcebergRow, DuckDbIcebergTimeoutError, DuckDbSvc, createDuckDbIcebergExecutor } from "./duckdb-iceberg-executor.mjs";
|
|
3
|
-
import { R2SqlClient, R2SqlClientConfig, R2SqlError, R2SqlQueryError, R2SqlResult, R2SqlRow, R2SqlTimeoutError, createR2SqlClient } from "./r2-sql-client.mjs";
|
|
3
|
+
import { R2SqlClient, R2SqlClientConfig, R2SqlError, R2SqlQueryError, R2SqlResult, R2SqlRow, R2SqlTimeoutError, createR2SqlClient, escapeSqlValue, inlineParams } from "./r2-sql-client.mjs";
|
|
4
4
|
import { ServerTailDispatcher, ServerTailDispatcherConfig, ServerTailEngine, ServerTailRoutingError, createServerTailDispatcher, resolveServerTailEngine, resolveServerTailEngineResult } from "./dispatcher.mjs";
|
|
5
|
-
|
|
5
|
+
import { R2SqlTransport, R2SqlTransportConfig, R2SqlTransportMetrics, R2SqlTransportResult, R2SqlTransportRow, createR2SqlTransport } from "./r2-sql-transport.mjs";
|
|
6
|
+
export { type ArchetypeSqlPlan, type BuildArchetypeSqlOptions, DuckDbIcebergError, type DuckDbIcebergExecutor, type DuckDbIcebergExecutorConfig, type DuckDbIcebergQueryError, type DuckDbIcebergResult, type DuckDbIcebergRow, DuckDbIcebergTimeoutError, type DuckDbSvc, type R2SqlClient, type R2SqlClientConfig, R2SqlError, type R2SqlQueryError, type R2SqlResult, type R2SqlRow, R2SqlTimeoutError, type R2SqlTransport, type R2SqlTransportConfig, type R2SqlTransportMetrics, type R2SqlTransportResult, type R2SqlTransportRow, type ServerTailDispatcher, type ServerTailDispatcherConfig, type ServerTailEngine, ServerTailRoutingError, TABLE_PLACEHOLDER, buildArchetypeSql, createDuckDbIcebergExecutor, createR2SqlClient, createR2SqlTransport, createServerTailDispatcher, escapeSqlValue, inlineParams, resolveServerTailEngine, resolveServerTailEngineResult };
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { TABLE_PLACEHOLDER, buildArchetypeSql } from "./archetype-sql.mjs";
|
|
2
2
|
import { ServerTailRoutingError, createServerTailDispatcher, resolveServerTailEngine, resolveServerTailEngineResult } from "./dispatcher.mjs";
|
|
3
3
|
import { DuckDbIcebergError, DuckDbIcebergTimeoutError, createDuckDbIcebergExecutor } from "./duckdb-iceberg-executor.mjs";
|
|
4
|
-
import {
|
|
5
|
-
|
|
4
|
+
import { createR2SqlTransport } from "./r2-sql-transport.mjs";
|
|
5
|
+
import { R2SqlError, R2SqlTimeoutError, createR2SqlClient, escapeSqlValue, inlineParams } from "./r2-sql-client.mjs";
|
|
6
|
+
export { DuckDbIcebergError, DuckDbIcebergTimeoutError, R2SqlError, R2SqlTimeoutError, ServerTailRoutingError, TABLE_PLACEHOLDER, buildArchetypeSql, createDuckDbIcebergExecutor, createR2SqlClient, createR2SqlTransport, createServerTailDispatcher, escapeSqlValue, inlineParams, resolveServerTailEngine, resolveServerTailEngineResult };
|
|
@@ -63,6 +63,18 @@ declare class R2SqlTimeoutError extends Error {
|
|
|
63
63
|
* modelled here; they keep throwing `R2SqlError` synchronously.
|
|
64
64
|
*/
|
|
65
65
|
type R2SqlQueryError = R2SqlError | R2SqlTimeoutError;
|
|
66
|
+
/**
|
|
67
|
+
* Escape a JS value for inline embedding in R2 SQL. R2 SQL has no bound-param
|
|
68
|
+
* channel, so `buildArchetypeSql`'s `?` placeholders are substituted here.
|
|
69
|
+
* Numbers go in bare; strings are single-quote-escaped; null → `NULL`.
|
|
70
|
+
*/
|
|
71
|
+
declare function escapeSqlValue(value: unknown): string;
|
|
72
|
+
/**
|
|
73
|
+
* Inline a plan's `?`-bound params into its SQL, in order. R2 SQL accepts only
|
|
74
|
+
* a literal query string. Quote-aware so a `?` inside a string literal is not
|
|
75
|
+
* mistaken for a placeholder.
|
|
76
|
+
*/
|
|
77
|
+
declare function inlineParams(sql: string, params: readonly unknown[]): string;
|
|
66
78
|
/** A configured R2 SQL client. */
|
|
67
79
|
interface R2SqlClient {
|
|
68
80
|
/** Run a raw SQL string (table reference already resolved). */
|
|
@@ -86,4 +98,4 @@ interface R2SqlClient {
|
|
|
86
98
|
* production; tests inject `fetchImpl` returning a recorded envelope.
|
|
87
99
|
*/
|
|
88
100
|
declare function createR2SqlClient(config: R2SqlClientConfig): R2SqlClient;
|
|
89
|
-
export { R2SqlClient, R2SqlClientConfig, R2SqlError, R2SqlQueryError, R2SqlResult, R2SqlRow, R2SqlTimeoutError, createR2SqlClient };
|
|
101
|
+
export { R2SqlClient, R2SqlClientConfig, R2SqlError, R2SqlQueryError, R2SqlResult, R2SqlRow, R2SqlTimeoutError, createR2SqlClient, escapeSqlValue, inlineParams };
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { TABLE_PLACEHOLDER, buildArchetypeSql } from "./archetype-sql.mjs";
|
|
2
|
+
import { createR2SqlTransport } from "./r2-sql-transport.mjs";
|
|
2
3
|
import { err, ok, unwrapResult } from "gscdump/result";
|
|
3
4
|
import { SEARCH_TYPE_INT } from "@gscdump/engine/iceberg";
|
|
4
5
|
function r2TableRef(namespace, table) {
|
|
@@ -21,8 +22,6 @@ var R2SqlTimeoutError = class extends Error {
|
|
|
21
22
|
function r2SqlErrorToException(error) {
|
|
22
23
|
return error;
|
|
23
24
|
}
|
|
24
|
-
const DEFAULT_API_BASE = "https://api.sql.cloudflarestorage.com/api/v1";
|
|
25
|
-
const DEFAULT_TIMEOUT_MS = 25e3;
|
|
26
25
|
const PARTITION_PREDICATE_RE = /\b(site_id|search_type)(\s*=)/g;
|
|
27
26
|
function workaroundPartitionEquality(sql) {
|
|
28
27
|
return sql.replace(PARTITION_PREDICATE_RE, (_m, col, eq) => `CONCAT(${col}, '')${eq}`);
|
|
@@ -63,21 +62,6 @@ function inlineParams(sql, params) {
|
|
|
63
62
|
if (paramIndex !== params.length) throw new R2SqlError(`SQL has ${paramIndex} ? placeholders but ${params.length} params supplied`);
|
|
64
63
|
return out;
|
|
65
64
|
}
|
|
66
|
-
function normalizeRows(result) {
|
|
67
|
-
if (!result) return [];
|
|
68
|
-
if (Array.isArray(result.rows)) return result.rows;
|
|
69
|
-
if (Array.isArray(result.columns) && Array.isArray(result.data)) {
|
|
70
|
-
const cols = result.columns;
|
|
71
|
-
return result.data.map((tuple) => {
|
|
72
|
-
const row = {};
|
|
73
|
-
cols.forEach((col, idx) => {
|
|
74
|
-
row[col] = tuple[idx] ?? null;
|
|
75
|
-
});
|
|
76
|
-
return row;
|
|
77
|
-
});
|
|
78
|
-
}
|
|
79
|
-
return [];
|
|
80
|
-
}
|
|
81
65
|
function coerceIntSiteId(siteId, mapper) {
|
|
82
66
|
const mapped = mapper ? mapper(siteId) : siteId;
|
|
83
67
|
const n = Number(mapped);
|
|
@@ -96,43 +80,24 @@ function withEncodedPartitions(query, encoding, siteIdMapper) {
|
|
|
96
80
|
};
|
|
97
81
|
}
|
|
98
82
|
function createR2SqlClient(config) {
|
|
99
|
-
const
|
|
100
|
-
const apiBase = config.apiBase ?? DEFAULT_API_BASE;
|
|
101
|
-
const timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
83
|
+
const timeoutMs = config.timeoutMs ?? 25e3;
|
|
102
84
|
const partitionKeyEncoding = config.partitionKeyEncoding ?? "int";
|
|
103
|
-
const
|
|
85
|
+
const transport = createR2SqlTransport({
|
|
86
|
+
accountId: config.accountId,
|
|
87
|
+
bucket: config.bucket,
|
|
88
|
+
token: config.token,
|
|
89
|
+
apiBase: config.apiBase,
|
|
90
|
+
fetchImpl: config.fetchImpl,
|
|
91
|
+
timeoutMs
|
|
92
|
+
});
|
|
104
93
|
async function queryResult(sql) {
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
let response;
|
|
109
|
-
try {
|
|
110
|
-
response = await fetchImpl(endpoint, {
|
|
111
|
-
method: "POST",
|
|
112
|
-
headers: {
|
|
113
|
-
"authorization": `Bearer ${config.token}`,
|
|
114
|
-
"content-type": "application/json",
|
|
115
|
-
"user-agent": "gscdump-cloudflare-r2sql/1.0"
|
|
116
|
-
},
|
|
117
|
-
body: JSON.stringify({ query: sql }),
|
|
118
|
-
signal: controller.signal
|
|
119
|
-
});
|
|
120
|
-
} catch (error) {
|
|
121
|
-
if (error instanceof R2SqlTimeoutError || error?.name === "AbortError") return err(new R2SqlTimeoutError(timeoutMs));
|
|
122
|
-
return err(new R2SqlError(`R2 SQL request failed: ${error.message}`));
|
|
123
|
-
} finally {
|
|
124
|
-
clearTimeout(timer);
|
|
125
|
-
}
|
|
126
|
-
if (!response.ok) {
|
|
127
|
-
const text = await response.text().catch(() => "");
|
|
128
|
-
return err(new R2SqlError(`R2 SQL HTTP ${response.status}: ${text}`, response.status));
|
|
129
|
-
}
|
|
130
|
-
const envelope = await response.json();
|
|
131
|
-
if (!envelope.success) return err(new R2SqlError(`R2 SQL query rejected: ${envelope.errors?.map((e) => e.message).join("; ") ?? "unknown R2 SQL error"}`));
|
|
94
|
+
const result = await transport.query(sql);
|
|
95
|
+
if (result._tag === "timeout") return err(new R2SqlTimeoutError(result.timeoutMs));
|
|
96
|
+
if (result._tag === "error") return err(new R2SqlError(result.message, result.status));
|
|
132
97
|
return ok({
|
|
133
|
-
rows:
|
|
98
|
+
rows: result.rows,
|
|
134
99
|
sql,
|
|
135
|
-
queryMs:
|
|
100
|
+
queryMs: result.queryMs
|
|
136
101
|
});
|
|
137
102
|
}
|
|
138
103
|
async function query(sql) {
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
type R2SqlTransportRow = Record<string, unknown>;
|
|
2
|
+
type R2SqlTransportMetrics = Record<string, number | undefined>;
|
|
3
|
+
type R2SqlTransportResult = {
|
|
4
|
+
_tag: 'ok';
|
|
5
|
+
rows: R2SqlTransportRow[];
|
|
6
|
+
metrics: R2SqlTransportMetrics | null;
|
|
7
|
+
sql: string;
|
|
8
|
+
queryMs: number;
|
|
9
|
+
} | {
|
|
10
|
+
_tag: 'timeout';
|
|
11
|
+
timeoutMs: number;
|
|
12
|
+
} | {
|
|
13
|
+
_tag: 'error';
|
|
14
|
+
kind: 'network' | 'http' | 'rejected' | 'invalid_response';
|
|
15
|
+
message: string;
|
|
16
|
+
status?: number;
|
|
17
|
+
body?: string;
|
|
18
|
+
retryAfterMs?: number;
|
|
19
|
+
};
|
|
20
|
+
interface R2SqlTransportConfig {
|
|
21
|
+
accountId: string;
|
|
22
|
+
bucket: string;
|
|
23
|
+
token: string;
|
|
24
|
+
apiBase?: string;
|
|
25
|
+
fetchImpl?: typeof fetch;
|
|
26
|
+
timeoutMs?: number;
|
|
27
|
+
userAgent?: string;
|
|
28
|
+
now?: () => number;
|
|
29
|
+
}
|
|
30
|
+
interface R2SqlTransport {
|
|
31
|
+
endpoint: string;
|
|
32
|
+
query: (sql: string) => Promise<R2SqlTransportResult>;
|
|
33
|
+
}
|
|
34
|
+
declare function createR2SqlTransport(config: R2SqlTransportConfig): R2SqlTransport;
|
|
35
|
+
export { R2SqlTransport, R2SqlTransportConfig, R2SqlTransportMetrics, R2SqlTransportResult, R2SqlTransportRow, createR2SqlTransport };
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
const DEFAULT_API_BASE = "https://api.sql.cloudflarestorage.com/api/v1";
|
|
2
|
+
const DEFAULT_TIMEOUT_MS = 25e3;
|
|
3
|
+
const MAX_RETRY_AFTER_MS = 6e4;
|
|
4
|
+
function errorMessage(error) {
|
|
5
|
+
return error instanceof Error ? error.message : String(error);
|
|
6
|
+
}
|
|
7
|
+
function parseEnvelope(value) {
|
|
8
|
+
return value && typeof value === "object" ? value : null;
|
|
9
|
+
}
|
|
10
|
+
function normalizeRows(result) {
|
|
11
|
+
if (Array.isArray(result?.rows)) return result.rows;
|
|
12
|
+
if (!Array.isArray(result?.columns) || !Array.isArray(result.data)) return [];
|
|
13
|
+
return result.data.map((tuple) => {
|
|
14
|
+
const row = {};
|
|
15
|
+
for (let index = 0; index < result.columns.length; index++) row[result.columns[index]] = tuple[index] ?? null;
|
|
16
|
+
return row;
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
function retryAfterMs(response, now) {
|
|
20
|
+
const raw = response.headers?.get?.("retry-after");
|
|
21
|
+
if (!raw) return void 0;
|
|
22
|
+
const seconds = Number(raw.trim());
|
|
23
|
+
const value = Number.isFinite(seconds) ? seconds * 1e3 : Date.parse(raw) - now();
|
|
24
|
+
if (!Number.isFinite(value) || value <= 0) return void 0;
|
|
25
|
+
return Math.min(value, MAX_RETRY_AFTER_MS);
|
|
26
|
+
}
|
|
27
|
+
function createR2SqlTransport(config) {
|
|
28
|
+
const fetchImpl = config.fetchImpl ?? globalThis.fetch;
|
|
29
|
+
const timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
30
|
+
const now = config.now ?? Date.now;
|
|
31
|
+
const endpoint = `${(config.apiBase ?? DEFAULT_API_BASE).replace(/\/+$/, "")}/accounts/${config.accountId}/r2-sql/query/${config.bucket}`;
|
|
32
|
+
async function query(sql) {
|
|
33
|
+
const startedAt = now();
|
|
34
|
+
const controller = new AbortController();
|
|
35
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
36
|
+
try {
|
|
37
|
+
const responseResult = await fetchImpl(endpoint, {
|
|
38
|
+
method: "POST",
|
|
39
|
+
headers: {
|
|
40
|
+
"authorization": `Bearer ${config.token}`,
|
|
41
|
+
"content-type": "application/json",
|
|
42
|
+
"user-agent": config.userAgent ?? "gscdump-cloudflare-r2sql/1.0"
|
|
43
|
+
},
|
|
44
|
+
body: JSON.stringify({ query: sql }),
|
|
45
|
+
signal: controller.signal
|
|
46
|
+
}).then((response) => ({
|
|
47
|
+
_tag: "ok",
|
|
48
|
+
response
|
|
49
|
+
}), (error) => ({
|
|
50
|
+
_tag: "error",
|
|
51
|
+
error
|
|
52
|
+
}));
|
|
53
|
+
if (responseResult._tag === "error") {
|
|
54
|
+
if (controller.signal.aborted || responseResult.error?.name === "AbortError") return {
|
|
55
|
+
_tag: "timeout",
|
|
56
|
+
timeoutMs
|
|
57
|
+
};
|
|
58
|
+
return {
|
|
59
|
+
_tag: "error",
|
|
60
|
+
kind: "network",
|
|
61
|
+
message: `R2 SQL request failed: ${errorMessage(responseResult.error)}`
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
const { response } = responseResult;
|
|
65
|
+
if (!response.ok) {
|
|
66
|
+
const bodyResult = await response.text().then((body) => ({
|
|
67
|
+
_tag: "ok",
|
|
68
|
+
body
|
|
69
|
+
}), (error) => ({
|
|
70
|
+
_tag: "error",
|
|
71
|
+
error
|
|
72
|
+
}));
|
|
73
|
+
if (bodyResult._tag === "error") return {
|
|
74
|
+
_tag: "error",
|
|
75
|
+
kind: "network",
|
|
76
|
+
message: `R2 SQL response read failed: ${errorMessage(bodyResult.error)}`,
|
|
77
|
+
status: response.status
|
|
78
|
+
};
|
|
79
|
+
const retryAfter = retryAfterMs(response, now);
|
|
80
|
+
return {
|
|
81
|
+
_tag: "error",
|
|
82
|
+
kind: "http",
|
|
83
|
+
message: `R2 SQL HTTP ${response.status}: ${bodyResult.body}`,
|
|
84
|
+
status: response.status,
|
|
85
|
+
body: bodyResult.body,
|
|
86
|
+
...retryAfter !== void 0 ? { retryAfterMs: retryAfter } : {}
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
const envelopeResult = await response.json().then((value) => ({
|
|
90
|
+
_tag: "ok",
|
|
91
|
+
value
|
|
92
|
+
}), (error) => ({
|
|
93
|
+
_tag: "error",
|
|
94
|
+
error
|
|
95
|
+
}));
|
|
96
|
+
if (envelopeResult._tag === "error") return {
|
|
97
|
+
_tag: "error",
|
|
98
|
+
kind: "invalid_response",
|
|
99
|
+
message: `R2 SQL returned invalid JSON: ${errorMessage(envelopeResult.error)}`,
|
|
100
|
+
status: response.status
|
|
101
|
+
};
|
|
102
|
+
const envelope = parseEnvelope(envelopeResult.value);
|
|
103
|
+
if (!envelope) return {
|
|
104
|
+
_tag: "error",
|
|
105
|
+
kind: "invalid_response",
|
|
106
|
+
message: "R2 SQL returned an invalid response",
|
|
107
|
+
status: response.status
|
|
108
|
+
};
|
|
109
|
+
if (!envelope.success) return {
|
|
110
|
+
_tag: "error",
|
|
111
|
+
kind: "rejected",
|
|
112
|
+
message: `R2 SQL query rejected: ${envelope.errors?.map((error) => error.message).filter(Boolean).join("; ") || "unknown R2 SQL error"}`,
|
|
113
|
+
status: response.status,
|
|
114
|
+
body: JSON.stringify(envelope)
|
|
115
|
+
};
|
|
116
|
+
return {
|
|
117
|
+
_tag: "ok",
|
|
118
|
+
rows: normalizeRows(envelope.result),
|
|
119
|
+
metrics: envelope.result?.metrics ?? null,
|
|
120
|
+
sql,
|
|
121
|
+
queryMs: now() - startedAt
|
|
122
|
+
};
|
|
123
|
+
} finally {
|
|
124
|
+
clearTimeout(timer);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return {
|
|
128
|
+
endpoint,
|
|
129
|
+
query
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
export { createR2SqlTransport };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gscdump/cloudflare",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "1.5.
|
|
4
|
+
"version": "1.5.3",
|
|
5
5
|
"description": "Cloudflare-Workers-flavored helpers for the gscdump analytics stack: AnalyticsEnv binding contract, R2 SigV4 presigner, DuckDB Workers shims, engine factory.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Harlan Wilton",
|
|
@@ -47,10 +47,10 @@
|
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
49
|
"@uwdata/flechette": "^2.5.0",
|
|
50
|
-
"@gscdump/contracts": "^1.5.
|
|
51
|
-
"@gscdump/engine": "^1.5.
|
|
52
|
-
"@gscdump/engine-sqlite": "^1.5.
|
|
53
|
-
"gscdump": "^1.5.
|
|
50
|
+
"@gscdump/contracts": "^1.5.3",
|
|
51
|
+
"@gscdump/engine": "^1.5.3",
|
|
52
|
+
"@gscdump/engine-sqlite": "^1.5.3",
|
|
53
|
+
"gscdump": "^1.5.3"
|
|
54
54
|
},
|
|
55
55
|
"devDependencies": {
|
|
56
56
|
"@cloudflare/vitest-pool-workers": "^0.18.8",
|