@lunora/bindings 0.0.0 → 1.0.0-alpha.10
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/LICENSE.md +111 -0
- package/README.md +39 -1
- package/__assets__/package-og.svg +14 -0
- package/dist/analytics/index.d.mts +148 -0
- package/dist/analytics/index.d.ts +148 -0
- package/dist/analytics/index.mjs +2 -0
- package/dist/images/index.d.mts +338 -0
- package/dist/images/index.d.ts +338 -0
- package/dist/images/index.mjs +3 -0
- package/dist/kv/index.d.mts +271 -0
- package/dist/kv/index.d.ts +271 -0
- package/dist/kv/index.mjs +2 -0
- package/dist/packem_shared/AnalyticsSqlError-C2nz3jpH.mjs +41 -0
- package/dist/packem_shared/R2SqlError-drPKSCZ3.mjs +65 -0
- package/dist/packem_shared/SelectBuilder-BOqJQHEv.mjs +168 -0
- package/dist/packem_shared/SetOperation-DmPgUL8W.mjs +81 -0
- package/dist/packem_shared/Sql-B3zq2YGx.mjs +74 -0
- package/dist/packem_shared/WindowExpression-BT_uA6g1.mjs +44 -0
- package/dist/packem_shared/WindowFunction-DrnuZUF6.mjs +82 -0
- package/dist/packem_shared/asc-DZbQCxh1.mjs +16 -0
- package/dist/packem_shared/buildImageDeliveryUrl-qZ7XbqTL.mjs +35 -0
- package/dist/packem_shared/buildSignedImageUrl-DNUFfyGP.mjs +130 -0
- package/dist/packem_shared/concurrent-CkCEVwqP.mjs +39 -0
- package/dist/packem_shared/createAnalytics-CEEI69o9.mjs +57 -0
- package/dist/packem_shared/createContextVectors-DwZtnPeC.mjs +140 -0
- package/dist/packem_shared/createImages-BzRnsz3H.mjs +85 -0
- package/dist/packem_shared/createKv-C8Iyu5hD.mjs +145 -0
- package/dist/packem_shared/createKvIntrospector-Byk4GfsY.mjs +77 -0
- package/dist/packem_shared/createPipelines-CfyJ6VGu.mjs +10 -0
- package/dist/packem_shared/createVectorAdminIntrospector-DuSvcBa5.mjs +53 -0
- package/dist/packem_shared/createVectors-CTSrctiK.mjs +95 -0
- package/dist/pipelines/index.d.mts +41 -0
- package/dist/pipelines/index.d.ts +41 -0
- package/dist/pipelines/index.mjs +1 -0
- package/dist/r2sql/index.d.mts +383 -0
- package/dist/r2sql/index.d.ts +383 -0
- package/dist/r2sql/index.mjs +7 -0
- package/dist/vectors/index.d.mts +285 -0
- package/dist/vectors/index.d.ts +285 -0
- package/dist/vectors/index.mjs +3 -0
- package/package.json +57 -4
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The value types Workers KV can store / return. Mirrors Cloudflare's
|
|
3
|
+
* `KVNamespace` `get`/`put` body unions; declared here so the package stays
|
|
4
|
+
* runtime-agnostic and the `*Like` interfaces don't pull in
|
|
5
|
+
* `@cloudflare/workers-types` at runtime.
|
|
6
|
+
*/
|
|
7
|
+
type KvValue = ReadableStream | ArrayBuffer | ArrayBufferView | string;
|
|
8
|
+
/** How a raw KV read should decode the stored value. Mirrors KV's `type` option. */
|
|
9
|
+
type KvValueType = "text" | "json" | "arrayBuffer" | "stream";
|
|
10
|
+
/**
|
|
11
|
+
* Per-read options forwarded to the binding. `cacheTtl` is KV's edge-cache TTL
|
|
12
|
+
* (seconds, min 60); `type` selects the decode mode for {@link Kv.getRaw}.
|
|
13
|
+
*/
|
|
14
|
+
interface KvGetOptions {
|
|
15
|
+
/** KV edge-cache TTL in seconds (minimum 60). Forwarded verbatim. */
|
|
16
|
+
cacheTtl?: number;
|
|
17
|
+
/** Decode mode for a raw read. {@link Kv.get} always uses `"json"`. */
|
|
18
|
+
type?: KvValueType;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Minimal projection of Cloudflare's `KVNamespace`. Declared structurally so
|
|
22
|
+
* unit tests can pass a plain `Map`-backed double; the real binding satisfies
|
|
23
|
+
* the same shape. Mirrors `R2BucketLike` in `@lunora/storage`.
|
|
24
|
+
*/
|
|
25
|
+
interface KVNamespaceLike {
|
|
26
|
+
/** Delete a key. No-op if the key is absent. */
|
|
27
|
+
delete: (key: string) => Promise<void>;
|
|
28
|
+
/**
|
|
29
|
+
* Read a value. The real binding overloads on `options.type`; declared here
|
|
30
|
+
* as the broad union so a structural double need only return the value (or
|
|
31
|
+
* `null` when absent).
|
|
32
|
+
*/
|
|
33
|
+
get: (key: string, options?: KvGetOptions | KvValueType) => Promise<unknown>;
|
|
34
|
+
/**
|
|
35
|
+
* Read a value together with its associated metadata. Returns
|
|
36
|
+
* `{ value: null, metadata: null }` when the key is absent.
|
|
37
|
+
*/
|
|
38
|
+
getWithMetadata: (key: string, options?: KvGetOptions | KvValueType) => Promise<{
|
|
39
|
+
metadata: unknown;
|
|
40
|
+
value: unknown;
|
|
41
|
+
}>;
|
|
42
|
+
/** List keys, optionally filtered by `prefix` and paginated via `cursor`. */
|
|
43
|
+
list: (options?: {
|
|
44
|
+
cursor?: string;
|
|
45
|
+
limit?: number;
|
|
46
|
+
prefix?: string;
|
|
47
|
+
}) => Promise<KvNamespaceListResult>;
|
|
48
|
+
/** Write a value, optionally with TTL/expiration and metadata. */
|
|
49
|
+
put: (key: string, value: KvValue, options?: KvNamespacePutOptions) => Promise<void>;
|
|
50
|
+
}
|
|
51
|
+
/** The raw put options the KV binding accepts (mirrors `KVNamespacePutOptions`). */
|
|
52
|
+
interface KvNamespacePutOptions {
|
|
53
|
+
/** Absolute expiration as a Unix timestamp (seconds). Mutually exclusive with `expirationTtl`. */
|
|
54
|
+
expiration?: number;
|
|
55
|
+
/** Relative expiration in seconds from now (minimum 60). Mutually exclusive with `expiration`. */
|
|
56
|
+
expirationTtl?: number;
|
|
57
|
+
/** Arbitrary JSON metadata stored alongside the value, returned by `getWithMetadata`/`list`. */
|
|
58
|
+
metadata?: unknown;
|
|
59
|
+
}
|
|
60
|
+
/** One key entry as returned by the KV binding's `list`. */
|
|
61
|
+
interface KvListKey<Metadata = unknown> {
|
|
62
|
+
/** Absolute expiration (Unix seconds), when the key has one. */
|
|
63
|
+
expiration?: number;
|
|
64
|
+
/** The key's metadata, when set at write time. */
|
|
65
|
+
metadata?: Metadata;
|
|
66
|
+
/** The key name. */
|
|
67
|
+
name: string;
|
|
68
|
+
}
|
|
69
|
+
/** The raw `list` result shape returned by the KV binding. */
|
|
70
|
+
type KvNamespaceListResult<Metadata = unknown> = {
|
|
71
|
+
cacheStatus?: string | null;
|
|
72
|
+
cursor: string;
|
|
73
|
+
keys: KvListKey<Metadata>[];
|
|
74
|
+
list_complete: false;
|
|
75
|
+
} | {
|
|
76
|
+
cacheStatus?: string | null;
|
|
77
|
+
keys: KvListKey<Metadata>[];
|
|
78
|
+
list_complete: true;
|
|
79
|
+
};
|
|
80
|
+
/** Construction options for the `createKv` factory. */
|
|
81
|
+
interface LunoraKvOptions {
|
|
82
|
+
/**
|
|
83
|
+
* Optional per-instance key prefix applied to every operation (get/put/
|
|
84
|
+
* delete/list). Use for multi-tenant key namespacing — equivalent to
|
|
85
|
+
* calling the `scopeKey` helper on every key. Combined via `scopeKey`, so a
|
|
86
|
+
* `..` or NUL in the prefix is rejected.
|
|
87
|
+
*/
|
|
88
|
+
keyPrefix?: string;
|
|
89
|
+
/** The bound KV namespace (`env.<BINDING>`). */
|
|
90
|
+
namespace: KVNamespaceLike;
|
|
91
|
+
}
|
|
92
|
+
/** Options for {@link Kv.put}. JSON-stringifies the value unless `raw` is set. */
|
|
93
|
+
interface KvPutOptions {
|
|
94
|
+
/** Absolute expiration as a Unix timestamp (seconds). Mutually exclusive with `expirationTtl`. */
|
|
95
|
+
expiration?: number;
|
|
96
|
+
/** Relative expiration in seconds from now (minimum 60). Mutually exclusive with `expiration`. */
|
|
97
|
+
expirationTtl?: number;
|
|
98
|
+
/** Arbitrary metadata stored alongside the value (returned by `getWithMetadata`/`list`). */
|
|
99
|
+
metadata?: unknown;
|
|
100
|
+
/**
|
|
101
|
+
* When true, write `value` to KV verbatim (no `JSON.stringify`). `value`
|
|
102
|
+
* must already be a KV-writable type (string/ArrayBuffer/stream).
|
|
103
|
+
*/
|
|
104
|
+
raw?: boolean;
|
|
105
|
+
}
|
|
106
|
+
/** Options for {@link Kv.list}. */
|
|
107
|
+
interface KvListOptions {
|
|
108
|
+
/** Opaque cursor from a previous truncated page. */
|
|
109
|
+
cursor?: string;
|
|
110
|
+
/** Max keys per page (KV caps at 1000). */
|
|
111
|
+
limit?: number;
|
|
112
|
+
/** Restrict to keys starting with this prefix (combined with any `keyPrefix`). */
|
|
113
|
+
prefix?: string;
|
|
114
|
+
}
|
|
115
|
+
/** A single page of {@link Kv.list} results. */
|
|
116
|
+
interface KvListResult<Metadata = unknown> {
|
|
117
|
+
/** Cursor for the next page; `undefined` when the listing is complete. */
|
|
118
|
+
cursor?: string;
|
|
119
|
+
/**
|
|
120
|
+
* The key names, with any instance `keyPrefix` stripped back off so callers
|
|
121
|
+
* see the same keys they wrote.
|
|
122
|
+
*/
|
|
123
|
+
keys: KvListKey<Metadata>[];
|
|
124
|
+
/** True when this is the final page (no further `cursor`). */
|
|
125
|
+
listComplete: boolean;
|
|
126
|
+
}
|
|
127
|
+
/** A value together with its stored metadata (from {@link Kv.getWithMetadata}). */
|
|
128
|
+
interface KvValueWithMetadata<Value, Metadata> {
|
|
129
|
+
/** The stored metadata, or `null` when none was set / the key is absent. */
|
|
130
|
+
metadata: Metadata | null;
|
|
131
|
+
/** The decoded value, or `null` when the key is absent. */
|
|
132
|
+
value: Value | null;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* The typed Workers KV client bound to `ctx.kv`. JSON-decodes/encodes by
|
|
136
|
+
* default; a raw escape hatch ({@link Kv.getRaw} / `put(..., { raw: true })`)
|
|
137
|
+
* handles text/binary/stream values.
|
|
138
|
+
*/
|
|
139
|
+
interface Kv {
|
|
140
|
+
/** Delete a key. No-op if absent. */
|
|
141
|
+
delete: (key: string) => Promise<void>;
|
|
142
|
+
/**
|
|
143
|
+
* Read a key and `JSON.parse` it into `T`. Returns `null` when the key is
|
|
144
|
+
* absent. Throws if the stored value isn't valid JSON — use
|
|
145
|
+
* {@link Kv.getRaw} for non-JSON values.
|
|
146
|
+
*/
|
|
147
|
+
get: <T = unknown>(key: string, options?: {
|
|
148
|
+
cacheTtl?: number;
|
|
149
|
+
}) => Promise<T | null>;
|
|
150
|
+
/** Read a raw value with an explicit decode `type` (default `"text"`). Returns `null` when absent. */
|
|
151
|
+
getRaw: <T = string>(key: string, options?: KvGetOptions) => Promise<T | null>;
|
|
152
|
+
/**
|
|
153
|
+
* Read a key's JSON value together with its metadata. Returns
|
|
154
|
+
* `{ value: null, metadata: null }` when the key is absent.
|
|
155
|
+
*/
|
|
156
|
+
getWithMetadata: <T = unknown, M = unknown>(key: string, options?: {
|
|
157
|
+
cacheTtl?: number;
|
|
158
|
+
}) => Promise<KvValueWithMetadata<T, M>>;
|
|
159
|
+
/** List keys (optionally `prefix`-filtered, paginated via `cursor`). */
|
|
160
|
+
list: <M = unknown>(options?: KvListOptions) => Promise<KvListResult<M>>;
|
|
161
|
+
/**
|
|
162
|
+
* Write `value` to `key`. JSON-stringifies `value` unless `options.raw` is
|
|
163
|
+
* set (in which case `value` must be a KV-writable type). Forwards
|
|
164
|
+
* `expirationTtl`/`expiration`/`metadata` to the binding.
|
|
165
|
+
*/
|
|
166
|
+
put: <T = unknown>(key: string, value: T, options?: KvPutOptions) => Promise<void>;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Compose a per-tenant key from a scope prefix and a caller-supplied key. Both
|
|
170
|
+
* halves are validated — the prefix may not contain `..` or NUL either, and the
|
|
171
|
+
* resulting key must stay under KV's length ceiling. Recommended for any
|
|
172
|
+
* multi-tenant deployment so client-supplied keys can't address peer data.
|
|
173
|
+
* Mirrors `scopeKey` from `@lunora/storage`.
|
|
174
|
+
*/
|
|
175
|
+
declare const scopeKey: (prefix: string, key: string) => string;
|
|
176
|
+
declare const createKv: (options: LunoraKvOptions) => Kv;
|
|
177
|
+
/** One KV namespace as the studio's KV browser surfaces it (mirrors the runtime's KvNamespaceSummary). */
|
|
178
|
+
interface KvNamespaceSummaryLike {
|
|
179
|
+
binding: string;
|
|
180
|
+
}
|
|
181
|
+
/** One key entry as surfaced by the KV admin browser (mirrors the runtime's KvKeyEntry). */
|
|
182
|
+
interface KvKeyEntryLike {
|
|
183
|
+
expiration?: number;
|
|
184
|
+
metadata?: unknown;
|
|
185
|
+
name: string;
|
|
186
|
+
}
|
|
187
|
+
/** A paginated page of KV keys (mirrors the runtime's KvKeyListResult). */
|
|
188
|
+
interface KvKeyListResultLike {
|
|
189
|
+
cursor?: string;
|
|
190
|
+
keys: KvKeyEntryLike[];
|
|
191
|
+
listComplete: boolean;
|
|
192
|
+
}
|
|
193
|
+
/** A value together with its metadata (mirrors the runtime's KvValueResult). */
|
|
194
|
+
interface KvValueResultLike {
|
|
195
|
+
metadata: unknown;
|
|
196
|
+
value: null | string;
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* The structural shape of a KV introspector — mirrors `KvIntrospector` from
|
|
200
|
+
* `@lunora/runtime` without importing it, keeping the dependency graph clean.
|
|
201
|
+
*/
|
|
202
|
+
interface KvIntrospectorLike {
|
|
203
|
+
deleteKey: (options: {
|
|
204
|
+
key: string;
|
|
205
|
+
namespace: string;
|
|
206
|
+
}) => Promise<void>;
|
|
207
|
+
getValue: (options: {
|
|
208
|
+
key: string;
|
|
209
|
+
namespace: string;
|
|
210
|
+
}) => Promise<KvValueResultLike>;
|
|
211
|
+
listKeys: (options: {
|
|
212
|
+
cursor?: string;
|
|
213
|
+
limit?: number;
|
|
214
|
+
namespace: string;
|
|
215
|
+
prefix?: string;
|
|
216
|
+
}) => Promise<KvKeyListResultLike>;
|
|
217
|
+
listNamespaces: () => Promise<KvNamespaceSummaryLike[]>;
|
|
218
|
+
putValue: (options: {
|
|
219
|
+
expiration?: number;
|
|
220
|
+
expirationTtl?: number;
|
|
221
|
+
key: string;
|
|
222
|
+
metadata?: unknown;
|
|
223
|
+
namespace: string;
|
|
224
|
+
value: string;
|
|
225
|
+
}) => Promise<void>;
|
|
226
|
+
}
|
|
227
|
+
/** Construction options for {@link createKvIntrospector}. */
|
|
228
|
+
interface CreateKvIntrospectorOptions {
|
|
229
|
+
/**
|
|
230
|
+
* Map of binding name → bound KV namespace. Each entry becomes one
|
|
231
|
+
* namespace the studio can browse. Example:
|
|
232
|
+
* ```ts
|
|
233
|
+
* createKvIntrospector({ namespaces: { MY_KV: env.MY_KV } })
|
|
234
|
+
* ```
|
|
235
|
+
*/
|
|
236
|
+
namespaces: Record<string, KVNamespaceLike>;
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Build a `KvIntrospector`-compatible object from a map of bound KV namespaces.
|
|
240
|
+
* Pass the result as `kvIntrospector` on `WorkerOptions` to enable the studio's
|
|
241
|
+
* KV browser (`/_lunora/admin/kv/*` endpoints).
|
|
242
|
+
* @example
|
|
243
|
+
* ```ts
|
|
244
|
+
* createWorker({
|
|
245
|
+
* // …
|
|
246
|
+
* kvIntrospector: createKvIntrospector({ namespaces: { MY_KV: env.MY_KV } }),
|
|
247
|
+
* });
|
|
248
|
+
* ```
|
|
249
|
+
*/
|
|
250
|
+
declare const createKvIntrospector: (options: CreateKvIntrospectorOptions) => KvIntrospectorLike;
|
|
251
|
+
/**
|
|
252
|
+
* Zero-config KV introspector: scan a worker `env` for every bound Workers KV
|
|
253
|
+
* namespace and register each under its binding name. Every `kv_namespaces` entry
|
|
254
|
+
* in `wrangler.jsonc` then appears in the studio's KV browser automatically — no
|
|
255
|
+
* hand-written `createKvIntrospector({ namespaces: … })` call, and any binding
|
|
256
|
+
* name (not just `KV`) and any number of namespaces light up. Non-KV bindings
|
|
257
|
+
* (R2, Durable Objects, queues, secrets, …) are skipped via {@link isKvNamespace}.
|
|
258
|
+
*
|
|
259
|
+
* Returns an introspector even when `env` holds no KV namespaces — its
|
|
260
|
+
* `listNamespaces()` resolves to `[]`, so the studio renders an empty state
|
|
261
|
+
* rather than a "not configured" error.
|
|
262
|
+
* @example
|
|
263
|
+
* ```ts
|
|
264
|
+
* createWorker({
|
|
265
|
+
* // …
|
|
266
|
+
* kvIntrospector: createKvIntrospectorFromEnv(env),
|
|
267
|
+
* });
|
|
268
|
+
* ```
|
|
269
|
+
*/
|
|
270
|
+
declare const createKvIntrospectorFromEnv: (env: unknown) => KvIntrospectorLike;
|
|
271
|
+
export { type CreateKvIntrospectorOptions, type KVNamespaceLike, type Kv, type KvGetOptions, type KvIntrospectorLike, type KvListKey, type KvListOptions, type KvListResult, type KvNamespaceListResult, type KvNamespacePutOptions, type KvPutOptions, type KvValue, type KvValueType, type KvValueWithMetadata, type LunoraKvOptions, createKv, createKvIntrospector, createKvIntrospectorFromEnv, scopeKey };
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { LunoraError } from '@lunora/errors';
|
|
2
|
+
|
|
3
|
+
const SQL_API_BASE = "https://api.cloudflare.com/client/v4/accounts";
|
|
4
|
+
class AnalyticsSqlError extends LunoraError {
|
|
5
|
+
constructor(status, body) {
|
|
6
|
+
super("ANALYTICS_SQL_ERROR", `Analytics Engine SQL API returned ${String(status)}: ${body}`, { name: "AnalyticsSqlError", status });
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
const createAnalyticsSqlClient = (config) => {
|
|
10
|
+
const fetchImpl = config.fetch ?? globalThis.fetch;
|
|
11
|
+
const endpoint = `${SQL_API_BASE}/${encodeURIComponent(config.accountId)}/analytics_engine/sql`;
|
|
12
|
+
const query = async (sql) => {
|
|
13
|
+
const response = await fetchImpl(endpoint, {
|
|
14
|
+
body: sql,
|
|
15
|
+
headers: {
|
|
16
|
+
Authorization: `Bearer ${config.apiToken}`,
|
|
17
|
+
"Content-Type": "text/plain"
|
|
18
|
+
},
|
|
19
|
+
method: "POST"
|
|
20
|
+
});
|
|
21
|
+
if (!response.ok) {
|
|
22
|
+
throw new AnalyticsSqlError(response.status, await response.text());
|
|
23
|
+
}
|
|
24
|
+
let raw;
|
|
25
|
+
try {
|
|
26
|
+
raw = await response.json();
|
|
27
|
+
} catch {
|
|
28
|
+
throw new AnalyticsSqlError(response.status, "Analytics Engine SQL API returned a non-JSON body.");
|
|
29
|
+
}
|
|
30
|
+
const body = raw;
|
|
31
|
+
const rows = body.data ?? [];
|
|
32
|
+
return {
|
|
33
|
+
columns: body.meta ?? [],
|
|
34
|
+
rowCount: body.rows ?? rows.length,
|
|
35
|
+
rows
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
return { query };
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export { AnalyticsSqlError, createAnalyticsSqlClient };
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { LunoraError } from '@lunora/errors';
|
|
2
|
+
import SelectBuilder from './SelectBuilder-BOqJQHEv.mjs';
|
|
3
|
+
import { ident, toText } from './Sql-B3zq2YGx.mjs';
|
|
4
|
+
|
|
5
|
+
const API_BASE = "https://api.sql.cloudflarestorage.com/api/v1/accounts";
|
|
6
|
+
const inferColumns = (rows) => {
|
|
7
|
+
if (rows[0] === void 0) {
|
|
8
|
+
return [];
|
|
9
|
+
}
|
|
10
|
+
return Object.keys(rows[0]).map((name) => {
|
|
11
|
+
return { name };
|
|
12
|
+
});
|
|
13
|
+
};
|
|
14
|
+
class R2SqlError extends LunoraError {
|
|
15
|
+
constructor(status, body) {
|
|
16
|
+
super("R2_SQL_ERROR", `R2 SQL query failed (${String(status)}): ${body}`, { name: "R2SqlError", status });
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
const createR2Sql = (config) => {
|
|
20
|
+
const fetchImpl = config.fetch ?? globalThis.fetch;
|
|
21
|
+
const base = config.endpoint ?? API_BASE;
|
|
22
|
+
const endpoint = `${base}/${encodeURIComponent(config.accountId)}/r2-sql/query/${encodeURIComponent(config.bucket)}`;
|
|
23
|
+
const warehouse = `${config.accountId}_${config.bucket}`;
|
|
24
|
+
const exec = async (statement) => {
|
|
25
|
+
const response = await fetchImpl(endpoint, {
|
|
26
|
+
body: JSON.stringify({ query: statement, warehouse }),
|
|
27
|
+
headers: {
|
|
28
|
+
Authorization: `Bearer ${config.apiToken}`,
|
|
29
|
+
"Content-Type": "application/json"
|
|
30
|
+
},
|
|
31
|
+
method: "POST"
|
|
32
|
+
});
|
|
33
|
+
if (!response.ok) {
|
|
34
|
+
throw new R2SqlError(response.status, await response.text());
|
|
35
|
+
}
|
|
36
|
+
let raw;
|
|
37
|
+
try {
|
|
38
|
+
raw = await response.json();
|
|
39
|
+
} catch {
|
|
40
|
+
throw new R2SqlError(response.status, "R2 SQL returned a non-JSON body.");
|
|
41
|
+
}
|
|
42
|
+
const body = raw;
|
|
43
|
+
if (body.success === false || body.errors !== void 0 && body.errors.length > 0) {
|
|
44
|
+
throw new R2SqlError(response.status, JSON.stringify(body.errors ?? body));
|
|
45
|
+
}
|
|
46
|
+
const rows = body.result?.rows ?? [];
|
|
47
|
+
return {
|
|
48
|
+
columns: body.result?.schema ?? inferColumns(rows),
|
|
49
|
+
rowCount: rows.length,
|
|
50
|
+
rows
|
|
51
|
+
};
|
|
52
|
+
};
|
|
53
|
+
return {
|
|
54
|
+
describe: async (table) => exec(`DESCRIBE ${ident(table)}`),
|
|
55
|
+
explain: async (statement, options) => exec(`EXPLAIN ${options?.format === "json" ? "FORMAT JSON " : ""}${toText(statement)}`),
|
|
56
|
+
// `SelectBuilder`'s constructor validates the table reference (allowing an
|
|
57
|
+
// optional `[AS] alias`), so no pre-validation here.
|
|
58
|
+
from: (table) => new SelectBuilder(exec, table),
|
|
59
|
+
query: async (statement) => exec(toText(statement)),
|
|
60
|
+
showDatabases: async () => exec("SHOW DATABASES"),
|
|
61
|
+
showTables: async (namespace) => exec(`SHOW TABLES IN ${ident(namespace)}`)
|
|
62
|
+
};
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export { R2SqlError, createR2Sql };
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { renderOrderTerm } from './asc-DZbQCxh1.mjs';
|
|
2
|
+
import SetOperation from './SetOperation-DmPgUL8W.mjs';
|
|
3
|
+
import { tableRef, toText, assertLimit, lit } from './Sql-B3zq2YGx.mjs';
|
|
4
|
+
|
|
5
|
+
const JOIN_KEYWORDS = {
|
|
6
|
+
cross: "CROSS JOIN",
|
|
7
|
+
full: "FULL OUTER JOIN",
|
|
8
|
+
inner: "INNER JOIN",
|
|
9
|
+
left: "LEFT JOIN",
|
|
10
|
+
right: "RIGHT JOIN"
|
|
11
|
+
};
|
|
12
|
+
class SelectBuilder {
|
|
13
|
+
exec;
|
|
14
|
+
table;
|
|
15
|
+
selectItems = [];
|
|
16
|
+
distinctFlag = false;
|
|
17
|
+
distinctOnItems = [];
|
|
18
|
+
joins = [];
|
|
19
|
+
whereConditions = [];
|
|
20
|
+
groupByItems = [];
|
|
21
|
+
havingConditions = [];
|
|
22
|
+
qualifyCondition;
|
|
23
|
+
orderByItems = [];
|
|
24
|
+
limitValue;
|
|
25
|
+
constructor(exec, table) {
|
|
26
|
+
this.exec = exec;
|
|
27
|
+
this.table = tableRef(table);
|
|
28
|
+
}
|
|
29
|
+
/** The `SELECT` list. Omit/empty for `SELECT *`. Items are columns, expressions, or aliased window fragments (`fn.rowNumber().over(...).as("rk")`). */
|
|
30
|
+
select(...items) {
|
|
31
|
+
this.selectItems.push(...items.map((item) => toText(item)));
|
|
32
|
+
return this;
|
|
33
|
+
}
|
|
34
|
+
/** `SELECT DISTINCT` — unique rows. */
|
|
35
|
+
distinct() {
|
|
36
|
+
this.distinctFlag = true;
|
|
37
|
+
return this;
|
|
38
|
+
}
|
|
39
|
+
/** `DISTINCT ON (cols)` — the first row per distinct combination, ordered by {@link orderBy}. */
|
|
40
|
+
distinctOn(...columns) {
|
|
41
|
+
this.distinctOnItems.push(...columns.map((column) => toText(column)));
|
|
42
|
+
return this;
|
|
43
|
+
}
|
|
44
|
+
/** `INNER JOIN table ON condition`. */
|
|
45
|
+
innerJoin(table, on) {
|
|
46
|
+
return this.addJoin("inner", table, on);
|
|
47
|
+
}
|
|
48
|
+
/** `LEFT JOIN table ON condition`. */
|
|
49
|
+
leftJoin(table, on) {
|
|
50
|
+
return this.addJoin("left", table, on);
|
|
51
|
+
}
|
|
52
|
+
/** `RIGHT JOIN table ON condition`. */
|
|
53
|
+
rightJoin(table, on) {
|
|
54
|
+
return this.addJoin("right", table, on);
|
|
55
|
+
}
|
|
56
|
+
/** `FULL OUTER JOIN table ON condition`. */
|
|
57
|
+
fullJoin(table, on) {
|
|
58
|
+
return this.addJoin("full", table, on);
|
|
59
|
+
}
|
|
60
|
+
/** `CROSS JOIN table` (no `ON`). */
|
|
61
|
+
crossJoin(table) {
|
|
62
|
+
return this.addJoin("cross", table);
|
|
63
|
+
}
|
|
64
|
+
/** Add `WHERE` condition(s). Multiple calls (and multiple args) are `AND`-ed. Bind values with the `sql` tag. */
|
|
65
|
+
where(...conditions) {
|
|
66
|
+
this.whereConditions.push(...conditions.map((condition) => toText(condition)));
|
|
67
|
+
return this;
|
|
68
|
+
}
|
|
69
|
+
/** `GROUP BY` column(s)/expression(s). */
|
|
70
|
+
groupBy(...columns) {
|
|
71
|
+
this.groupByItems.push(...columns.map((column) => toText(column)));
|
|
72
|
+
return this;
|
|
73
|
+
}
|
|
74
|
+
/** Add `HAVING` condition(s) over aggregates; multiple are `AND`-ed. */
|
|
75
|
+
having(...conditions) {
|
|
76
|
+
this.havingConditions.push(...conditions.map((condition) => toText(condition)));
|
|
77
|
+
return this;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* `QUALIFY` — filter on a window function without a subquery, e.g.
|
|
81
|
+
* `.qualify(fn.rowNumber().over({ partitionBy: "region", orderBy: desc("total") }).lte(3))`.
|
|
82
|
+
*/
|
|
83
|
+
qualify(condition) {
|
|
84
|
+
this.qualifyCondition = toText(condition);
|
|
85
|
+
return this;
|
|
86
|
+
}
|
|
87
|
+
/** `ORDER BY` term(s) — bare strings (ASC) or {@link import("./order").asc | asc}/{@link import("./order").desc | desc} tags. */
|
|
88
|
+
orderBy(...terms) {
|
|
89
|
+
this.orderByItems.push(...terms.map((term) => renderOrderTerm(term)));
|
|
90
|
+
return this;
|
|
91
|
+
}
|
|
92
|
+
/** `LIMIT n` (R2 SQL: 1–10,000, default 500). */
|
|
93
|
+
limit(n) {
|
|
94
|
+
assertLimit(n);
|
|
95
|
+
this.limitValue = n;
|
|
96
|
+
return this;
|
|
97
|
+
}
|
|
98
|
+
/** Re-type the result rows without changing the query (the builder carries no schema of its own). */
|
|
99
|
+
returns() {
|
|
100
|
+
return this;
|
|
101
|
+
}
|
|
102
|
+
/** `this UNION other` — all rows from both, duplicates removed. */
|
|
103
|
+
union(other) {
|
|
104
|
+
return this.setOperation("UNION", other);
|
|
105
|
+
}
|
|
106
|
+
/** `this UNION ALL other` — all rows from both, duplicates kept. */
|
|
107
|
+
unionAll(other) {
|
|
108
|
+
return this.setOperation("UNION ALL", other);
|
|
109
|
+
}
|
|
110
|
+
/** `this INTERSECT other` — rows present in both. */
|
|
111
|
+
intersect(other) {
|
|
112
|
+
return this.setOperation("INTERSECT", other);
|
|
113
|
+
}
|
|
114
|
+
/** `this EXCEPT other` — rows in `this` but not `other`. */
|
|
115
|
+
except(other) {
|
|
116
|
+
return this.setOperation("EXCEPT", other);
|
|
117
|
+
}
|
|
118
|
+
/** True when this query carries its own `ORDER BY`/`LIMIT` — so a set operation must parenthesise it. */
|
|
119
|
+
get needsWrapForSetOperation() {
|
|
120
|
+
return this.orderByItems.length > 0 || this.limitValue !== void 0;
|
|
121
|
+
}
|
|
122
|
+
/** Render the `SELECT` statement (no trailing semicolon). */
|
|
123
|
+
toSQL() {
|
|
124
|
+
const parts = [`${this.renderHead()} ${this.selectItems.length > 0 ? this.selectItems.join(", ") : "*"}`, `FROM ${this.table}`];
|
|
125
|
+
for (const join of this.joins) {
|
|
126
|
+
parts.push(join.on === void 0 ? `${JOIN_KEYWORDS[join.kind]} ${join.table}` : `${JOIN_KEYWORDS[join.kind]} ${join.table} ON ${toText(join.on)}`);
|
|
127
|
+
}
|
|
128
|
+
if (this.whereConditions.length > 0) {
|
|
129
|
+
parts.push(`WHERE ${this.whereConditions.join(" AND ")}`);
|
|
130
|
+
}
|
|
131
|
+
if (this.groupByItems.length > 0) {
|
|
132
|
+
parts.push(`GROUP BY ${this.groupByItems.join(", ")}`);
|
|
133
|
+
}
|
|
134
|
+
if (this.havingConditions.length > 0) {
|
|
135
|
+
parts.push(`HAVING ${this.havingConditions.join(" AND ")}`);
|
|
136
|
+
}
|
|
137
|
+
if (this.qualifyCondition !== void 0) {
|
|
138
|
+
parts.push(`QUALIFY ${this.qualifyCondition}`);
|
|
139
|
+
}
|
|
140
|
+
if (this.orderByItems.length > 0) {
|
|
141
|
+
parts.push(`ORDER BY ${this.orderByItems.join(", ")}`);
|
|
142
|
+
}
|
|
143
|
+
if (this.limitValue !== void 0) {
|
|
144
|
+
parts.push(`LIMIT ${lit(this.limitValue)}`);
|
|
145
|
+
}
|
|
146
|
+
return parts.join(" ");
|
|
147
|
+
}
|
|
148
|
+
/** Execute the query and return the typed result. */
|
|
149
|
+
async run() {
|
|
150
|
+
return this.exec(this.toSQL());
|
|
151
|
+
}
|
|
152
|
+
/** The `SELECT [DISTINCT [ON (...)]]` head. */
|
|
153
|
+
renderHead() {
|
|
154
|
+
if (this.distinctOnItems.length > 0) {
|
|
155
|
+
return `SELECT DISTINCT ON (${this.distinctOnItems.join(", ")})`;
|
|
156
|
+
}
|
|
157
|
+
return this.distinctFlag ? "SELECT DISTINCT" : "SELECT";
|
|
158
|
+
}
|
|
159
|
+
addJoin(kind, table, on) {
|
|
160
|
+
this.joins.push({ kind, on, table: tableRef(table) });
|
|
161
|
+
return this;
|
|
162
|
+
}
|
|
163
|
+
setOperation(operator, other) {
|
|
164
|
+
return new SetOperation(this.exec, [{ query: this }, { operator, query: other }]);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export { SelectBuilder as default };
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { renderOrderTerm } from './asc-DZbQCxh1.mjs';
|
|
2
|
+
import { assertLimit, lit } from './Sql-B3zq2YGx.mjs';
|
|
3
|
+
|
|
4
|
+
const renderMember = (query) => {
|
|
5
|
+
const text = query.toSQL();
|
|
6
|
+
return query.needsWrapForSetOperation === true ? `(${text})` : text;
|
|
7
|
+
};
|
|
8
|
+
class SetOperation {
|
|
9
|
+
/**
|
|
10
|
+
* Always `true`: a nested set operation must be parenthesised when it is a
|
|
11
|
+
* member of another set operation, or mixed operators mis-associate — e.g.
|
|
12
|
+
* `a.union(b.except(c))` must render `a UNION (b EXCEPT c)`, not the flat
|
|
13
|
+
* `a UNION b EXCEPT c`.
|
|
14
|
+
*/
|
|
15
|
+
needsWrapForSetOperation = true;
|
|
16
|
+
exec;
|
|
17
|
+
members;
|
|
18
|
+
orderByItems = [];
|
|
19
|
+
limitValue;
|
|
20
|
+
constructor(exec, members) {
|
|
21
|
+
this.exec = exec;
|
|
22
|
+
this.members = members;
|
|
23
|
+
}
|
|
24
|
+
/** Append `UNION other`. */
|
|
25
|
+
union(other) {
|
|
26
|
+
return this.add("UNION", other);
|
|
27
|
+
}
|
|
28
|
+
/** Append `UNION ALL other`. */
|
|
29
|
+
unionAll(other) {
|
|
30
|
+
return this.add("UNION ALL", other);
|
|
31
|
+
}
|
|
32
|
+
/** Append `INTERSECT other`. */
|
|
33
|
+
intersect(other) {
|
|
34
|
+
return this.add("INTERSECT", other);
|
|
35
|
+
}
|
|
36
|
+
/** Append `EXCEPT other`. */
|
|
37
|
+
except(other) {
|
|
38
|
+
return this.add("EXCEPT", other);
|
|
39
|
+
}
|
|
40
|
+
/** `ORDER BY` applied to the combined result. */
|
|
41
|
+
orderBy(...terms) {
|
|
42
|
+
this.orderByItems.push(...terms.map((term) => renderOrderTerm(term)));
|
|
43
|
+
return this;
|
|
44
|
+
}
|
|
45
|
+
/** `LIMIT` applied to the combined result. */
|
|
46
|
+
limit(n) {
|
|
47
|
+
assertLimit(n);
|
|
48
|
+
this.limitValue = n;
|
|
49
|
+
return this;
|
|
50
|
+
}
|
|
51
|
+
/** Re-type the combined result rows. */
|
|
52
|
+
returns() {
|
|
53
|
+
return this;
|
|
54
|
+
}
|
|
55
|
+
/** Render the combined statement. */
|
|
56
|
+
toSQL() {
|
|
57
|
+
const parts = this.members.map((member) => {
|
|
58
|
+
const text = renderMember(member.query);
|
|
59
|
+
return member.operator === void 0 ? text : `${member.operator} ${text}`;
|
|
60
|
+
});
|
|
61
|
+
const combined = parts.join(" ");
|
|
62
|
+
const tail = [];
|
|
63
|
+
if (this.orderByItems.length > 0) {
|
|
64
|
+
tail.push(`ORDER BY ${this.orderByItems.join(", ")}`);
|
|
65
|
+
}
|
|
66
|
+
if (this.limitValue !== void 0) {
|
|
67
|
+
tail.push(`LIMIT ${lit(this.limitValue)}`);
|
|
68
|
+
}
|
|
69
|
+
return tail.length > 0 ? `${combined} ${tail.join(" ")}` : combined;
|
|
70
|
+
}
|
|
71
|
+
/** Execute the combined query and return the typed result. */
|
|
72
|
+
async run() {
|
|
73
|
+
return this.exec(this.toSQL());
|
|
74
|
+
}
|
|
75
|
+
add(operator, other) {
|
|
76
|
+
this.members.push({ operator, query: other });
|
|
77
|
+
return this;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export { SetOperation as default };
|