@lunora/bindings 0.0.0 → 1.0.0-alpha.2
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 +105 -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 +177 -0
- package/dist/kv/index.d.ts +177 -0
- package/dist/kv/index.mjs +1 -0
- package/dist/packem_shared/AnalyticsSqlError-CGTdsi4H.mjs +42 -0
- package/dist/packem_shared/R2SqlError-DlDd_SrE.mjs +67 -0
- package/dist/packem_shared/SelectBuilder-DHaXZwn_.mjs +167 -0
- package/dist/packem_shared/SetOperation-RDHcxccj.mjs +80 -0
- package/dist/packem_shared/Sql-DceGtcUd.mjs +68 -0
- package/dist/packem_shared/WindowExpression-Cg9s2xcr.mjs +44 -0
- package/dist/packem_shared/WindowFunction-DA3pGC3N.mjs +82 -0
- package/dist/packem_shared/asc-Cur-xO8v.mjs +16 -0
- package/dist/packem_shared/buildImageDeliveryUrl-D1sVfIOP.mjs +30 -0
- package/dist/packem_shared/buildSignedImageUrl-Otdgc_jO.mjs +113 -0
- package/dist/packem_shared/concurrent-Dj5sOibv.mjs +23 -0
- package/dist/packem_shared/createAnalytics-CEEI69o9.mjs +57 -0
- package/dist/packem_shared/createContextVectors-BSizpmu5.mjs +140 -0
- package/dist/packem_shared/createImages-CJrvqX0u.mjs +80 -0
- package/dist/packem_shared/createKv-DTiSt216.mjs +141 -0
- package/dist/packem_shared/createPipelines-CfyJ6VGu.mjs +10 -0
- package/dist/packem_shared/createVectorAdminIntrospector-BJUOM6VW.mjs +51 -0
- package/dist/packem_shared/createVectors-LSpGoKCd.mjs +91 -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 +54 -4
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { renderOrderTerm } from './asc-Cur-xO8v.mjs';
|
|
2
|
+
import { lit, toText } from './Sql-DceGtcUd.mjs';
|
|
3
|
+
import WindowExpression from './WindowExpression-Cg9s2xcr.mjs';
|
|
4
|
+
|
|
5
|
+
const toArray = (value) => {
|
|
6
|
+
if (value === void 0) {
|
|
7
|
+
return [];
|
|
8
|
+
}
|
|
9
|
+
return Array.isArray(value) ? value : [value];
|
|
10
|
+
};
|
|
11
|
+
const renderOver = (spec) => {
|
|
12
|
+
const clauses = [];
|
|
13
|
+
const partitions = toArray(spec.partitionBy).map((part) => toText(part));
|
|
14
|
+
if (partitions.length > 0) {
|
|
15
|
+
clauses.push(`PARTITION BY ${partitions.join(", ")}`);
|
|
16
|
+
}
|
|
17
|
+
const orders = toArray(spec.orderBy).map((order) => renderOrderTerm(order));
|
|
18
|
+
if (orders.length > 0) {
|
|
19
|
+
clauses.push(`ORDER BY ${orders.join(", ")}`);
|
|
20
|
+
}
|
|
21
|
+
if (spec.frame !== void 0 && spec.frame.length > 0) {
|
|
22
|
+
clauses.push(spec.frame);
|
|
23
|
+
}
|
|
24
|
+
return `OVER (${clauses.join(" ")})`;
|
|
25
|
+
};
|
|
26
|
+
const offsetArguments = (column, offset, fallback) => {
|
|
27
|
+
const parts = [toText(column)];
|
|
28
|
+
if (offset !== void 0) {
|
|
29
|
+
parts.push(lit(offset));
|
|
30
|
+
}
|
|
31
|
+
if (fallback !== void 0) {
|
|
32
|
+
parts.push(lit(fallback));
|
|
33
|
+
}
|
|
34
|
+
return parts.join(", ");
|
|
35
|
+
};
|
|
36
|
+
class WindowFunction {
|
|
37
|
+
callText;
|
|
38
|
+
constructor(callText) {
|
|
39
|
+
this.callText = callText;
|
|
40
|
+
}
|
|
41
|
+
/** Attach the window frame, yielding a {@link WindowExpression}. */
|
|
42
|
+
over(spec = {}) {
|
|
43
|
+
return new WindowExpression(`${this.callText} ${renderOver(spec)}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
const windowFunction = (callText) => new WindowFunction(callText);
|
|
47
|
+
const fn = {
|
|
48
|
+
/** `AVG(column) OVER (...)`. */
|
|
49
|
+
avg: (column) => windowFunction(`AVG(${toText(column)})`),
|
|
50
|
+
/** `COUNT(column) OVER (...)` — omit the column for `COUNT(*)`. */
|
|
51
|
+
count: (column) => windowFunction(`COUNT(${column === void 0 ? "*" : toText(column)})`),
|
|
52
|
+
/** `CUME_DIST() OVER (...)`. */
|
|
53
|
+
cumeDist: () => windowFunction("CUME_DIST()"),
|
|
54
|
+
/** `DENSE_RANK() OVER (...)`. */
|
|
55
|
+
denseRank: () => windowFunction("DENSE_RANK()"),
|
|
56
|
+
/** `FIRST_VALUE(column) OVER (...)`. */
|
|
57
|
+
firstValue: (column) => windowFunction(`FIRST_VALUE(${toText(column)})`),
|
|
58
|
+
/** `LAG(column[, offset[, default]]) OVER (...)`. */
|
|
59
|
+
lag: (column, offset, fallback) => windowFunction(`LAG(${offsetArguments(column, offset, fallback)})`),
|
|
60
|
+
/** `LAST_VALUE(column) OVER (...)`. */
|
|
61
|
+
lastValue: (column) => windowFunction(`LAST_VALUE(${toText(column)})`),
|
|
62
|
+
/** `LEAD(column[, offset[, default]]) OVER (...)`. */
|
|
63
|
+
lead: (column, offset, fallback) => windowFunction(`LEAD(${offsetArguments(column, offset, fallback)})`),
|
|
64
|
+
/** `MAX(column) OVER (...)`. */
|
|
65
|
+
max: (column) => windowFunction(`MAX(${toText(column)})`),
|
|
66
|
+
/** `MIN(column) OVER (...)`. */
|
|
67
|
+
min: (column) => windowFunction(`MIN(${toText(column)})`),
|
|
68
|
+
/** `NTH_VALUE(column, n) OVER (...)`. */
|
|
69
|
+
nthValue: (column, n) => windowFunction(`NTH_VALUE(${toText(column)}, ${lit(n)})`),
|
|
70
|
+
/** `NTILE(buckets) OVER (...)`. */
|
|
71
|
+
ntile: (buckets) => windowFunction(`NTILE(${lit(buckets)})`),
|
|
72
|
+
/** `PERCENT_RANK() OVER (...)`. */
|
|
73
|
+
percentRank: () => windowFunction("PERCENT_RANK()"),
|
|
74
|
+
/** `RANK() OVER (...)`. */
|
|
75
|
+
rank: () => windowFunction("RANK()"),
|
|
76
|
+
/** `ROW_NUMBER() OVER (...)`. */
|
|
77
|
+
rowNumber: () => windowFunction("ROW_NUMBER()"),
|
|
78
|
+
/** `SUM(column) OVER (...)`. */
|
|
79
|
+
sum: (column) => windowFunction(`SUM(${toText(column)})`)
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export { WindowFunction, fn };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Sql, toText } from './Sql-DceGtcUd.mjs';
|
|
2
|
+
|
|
3
|
+
const asc = (expr) => {
|
|
4
|
+
return { dir: "ASC", expr };
|
|
5
|
+
};
|
|
6
|
+
const desc = (expr) => {
|
|
7
|
+
return { dir: "DESC", expr };
|
|
8
|
+
};
|
|
9
|
+
const renderOrderTerm = (term) => {
|
|
10
|
+
if (typeof term === "string" || term instanceof Sql) {
|
|
11
|
+
return toText(term);
|
|
12
|
+
}
|
|
13
|
+
return `${toText(term.expr)} ${term.dir}`;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export { asc, desc, renderOrderTerm };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
const ABSOLUTE_URL_RE = /^[a-z][a-z\d+\-.]*:\/\//i;
|
|
2
|
+
const stripTrailingSlash = (value) => value.endsWith("/") ? value.slice(0, -1) : value;
|
|
3
|
+
const stripLeadingSlash = (value) => value.startsWith("/") ? value.slice(1) : value;
|
|
4
|
+
const serializeTransform = (transform) => Object.entries(transform).filter(([, value]) => value !== void 0 && (typeof value === "string" || typeof value === "number")).map(([key, value]) => {
|
|
5
|
+
const serialized = String(value);
|
|
6
|
+
if (serialized.includes(",") || serialized.includes("=")) {
|
|
7
|
+
throw new Error(
|
|
8
|
+
`@lunora/bindings/images: transform option \`${key}\` value \`${serialized}\` contains a \`,\` or \`=\`, which the /cdn-cgi/image/ option list cannot represent (these are the option/key-value separators). For colors, use the hex form (e.g. \`#RRGGBB\`/\`%23RRGGBB\`) instead of \`rgb(r,g,b)\`.`
|
|
9
|
+
);
|
|
10
|
+
}
|
|
11
|
+
return `${key}=${serialized}`;
|
|
12
|
+
}).join(",");
|
|
13
|
+
const encodeKey = (key) => key.split("/").map((segment) => encodeURIComponent(segment)).join("/");
|
|
14
|
+
const buildImageDeliveryUrl = (options) => {
|
|
15
|
+
const base = stripTrailingSlash(options.baseUrl);
|
|
16
|
+
if (options.imageId !== void 0) {
|
|
17
|
+
const variant = options.variant ?? "public";
|
|
18
|
+
return `${base}/${encodeURIComponent(options.imageId)}/${encodeURIComponent(variant)}`;
|
|
19
|
+
}
|
|
20
|
+
if (options.key === void 0) {
|
|
21
|
+
throw new Error("@lunora/bindings/images: buildImageDeliveryUrl requires either `imageId` or `key`");
|
|
22
|
+
}
|
|
23
|
+
const optionString = options.transform === void 0 ? "" : serializeTransform(options.transform);
|
|
24
|
+
const isAbsolute = ABSOLUTE_URL_RE.test(options.key);
|
|
25
|
+
const source = isAbsolute ? options.key : `/${encodeKey(stripLeadingSlash(options.key))}`;
|
|
26
|
+
const prefix = optionString === "" ? "/cdn-cgi/image" : `/cdn-cgi/image/${optionString}`;
|
|
27
|
+
return `${base}${prefix}${source.startsWith("/") ? source : `/${source}`}`;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export { buildImageDeliveryUrl };
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
const textEncoder = new TextEncoder();
|
|
2
|
+
const MAX_EXPIRES_IN_SECONDS = 7 * 24 * 60 * 60;
|
|
3
|
+
const SCHEME_PREFIX_RE = /^[a-z][a-z\d+\-.]*:\/\//i;
|
|
4
|
+
const LEADING_SLASH_RE = /^\//;
|
|
5
|
+
const toBase64Url = (bytes) => {
|
|
6
|
+
const binary = String.fromCodePoint(...bytes);
|
|
7
|
+
return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replaceAll("=", "");
|
|
8
|
+
};
|
|
9
|
+
const fromBase64Url = (input) => {
|
|
10
|
+
const padded = input.replaceAll("-", "+").replaceAll("_", "/") + "===".slice((input.length + 3) % 4);
|
|
11
|
+
const binary = atob(padded);
|
|
12
|
+
const bytes = new Uint8Array(binary.length);
|
|
13
|
+
for (let index = 0; index < binary.length; index += 1) {
|
|
14
|
+
bytes[index] = binary.codePointAt(index) ?? 0;
|
|
15
|
+
}
|
|
16
|
+
return bytes;
|
|
17
|
+
};
|
|
18
|
+
const KEY_CACHE_MAX = 64;
|
|
19
|
+
const keyCache = /* @__PURE__ */ new Map();
|
|
20
|
+
const importHmacKey = async (secret) => {
|
|
21
|
+
const cached = keyCache.get(secret);
|
|
22
|
+
if (cached) {
|
|
23
|
+
return cached;
|
|
24
|
+
}
|
|
25
|
+
if (keyCache.size >= KEY_CACHE_MAX) {
|
|
26
|
+
const oldest = keyCache.keys().next().value;
|
|
27
|
+
if (oldest !== void 0) {
|
|
28
|
+
keyCache.delete(oldest);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
const keyPromise = crypto.subtle.importKey("raw", textEncoder.encode(secret), { hash: "SHA-256", name: "HMAC" }, false, ["sign", "verify"]);
|
|
32
|
+
keyCache.set(secret, keyPromise);
|
|
33
|
+
return keyPromise;
|
|
34
|
+
};
|
|
35
|
+
const serializeTransform = (transform) => {
|
|
36
|
+
if (transform === void 0) {
|
|
37
|
+
return "";
|
|
38
|
+
}
|
|
39
|
+
return Object.entries(transform).filter(([, value]) => value !== void 0).toSorted(([a], [b]) => (a > b ? 1 : 0) - (a < b ? 1 : 0)).map(([key, value]) => `${key}=${typeof value === "object" ? JSON.stringify(value) : String(value)}`).join("&");
|
|
40
|
+
};
|
|
41
|
+
const canonicalize = (host, key, exp, transform) => `${host.toLowerCase()}
|
|
42
|
+
${key}
|
|
43
|
+
${String(exp)}
|
|
44
|
+
${transform}`;
|
|
45
|
+
const extractHost = (input) => {
|
|
46
|
+
try {
|
|
47
|
+
return new URL(input).host;
|
|
48
|
+
} catch {
|
|
49
|
+
const noScheme = input.replace(SCHEME_PREFIX_RE, "");
|
|
50
|
+
return noScheme.split("/")[0] ?? "";
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
const encodeKey = (key) => key.split("/").map((segment) => encodeURIComponent(segment)).join("/");
|
|
54
|
+
const buildSignedImageUrl = async (options) => {
|
|
55
|
+
const expiresInSeconds = options.expiresInSeconds ?? 60 * 60;
|
|
56
|
+
if (!Number.isFinite(expiresInSeconds) || expiresInSeconds <= 0) {
|
|
57
|
+
throw new Error("@lunora/bindings/images: expiresInSeconds must be a positive finite number");
|
|
58
|
+
}
|
|
59
|
+
if (expiresInSeconds > MAX_EXPIRES_IN_SECONDS) {
|
|
60
|
+
throw new Error(`@lunora/bindings/images: expiresInSeconds must not exceed ${String(MAX_EXPIRES_IN_SECONDS)} (7 days)`);
|
|
61
|
+
}
|
|
62
|
+
const exp = Math.floor(Date.now() / 1e3) + expiresInSeconds;
|
|
63
|
+
const host = extractHost(options.baseUrl);
|
|
64
|
+
const transform = serializeTransform(options.transform);
|
|
65
|
+
const normalizedKey = options.key.replace(LEADING_SLASH_RE, "");
|
|
66
|
+
const cryptoKey = await importHmacKey(options.secret);
|
|
67
|
+
const signature = await crypto.subtle.sign("HMAC", cryptoKey, textEncoder.encode(canonicalize(host, normalizedKey, exp, transform)));
|
|
68
|
+
const sig = toBase64Url(new Uint8Array(signature));
|
|
69
|
+
const base = options.baseUrl.endsWith("/") ? options.baseUrl.slice(0, -1) : options.baseUrl;
|
|
70
|
+
const safeKey = encodeKey(normalizedKey);
|
|
71
|
+
const tParameter = transform === "" ? "" : `&t=${encodeURIComponent(transform)}`;
|
|
72
|
+
return `${base}/${safeKey}?exp=${String(exp)}&sig=${sig}${tParameter}`;
|
|
73
|
+
};
|
|
74
|
+
const verifySignedImageUrl = async (input, secret, options) => {
|
|
75
|
+
let url;
|
|
76
|
+
try {
|
|
77
|
+
url = input instanceof URL ? input : new URL(input);
|
|
78
|
+
} catch {
|
|
79
|
+
return { reason: "malformed", valid: false };
|
|
80
|
+
}
|
|
81
|
+
const expRaw = url.searchParams.get("exp");
|
|
82
|
+
const exp = expRaw === null ? Number.NaN : Number(expRaw);
|
|
83
|
+
const sig = url.searchParams.get("sig");
|
|
84
|
+
const transform = url.searchParams.get("t") ?? "";
|
|
85
|
+
if (!sig || !Number.isInteger(exp)) {
|
|
86
|
+
return { reason: "malformed", valid: false };
|
|
87
|
+
}
|
|
88
|
+
if (exp < Math.floor(Date.now() / 1e3)) {
|
|
89
|
+
return { reason: "expired", valid: false };
|
|
90
|
+
}
|
|
91
|
+
let key;
|
|
92
|
+
let sigBytes;
|
|
93
|
+
try {
|
|
94
|
+
key = url.pathname.replace(LEADING_SLASH_RE, "").split("/").map((segment) => decodeURIComponent(segment)).join("/");
|
|
95
|
+
sigBytes = fromBase64Url(sig);
|
|
96
|
+
} catch {
|
|
97
|
+
return { reason: "malformed", valid: false };
|
|
98
|
+
}
|
|
99
|
+
const host = options?.expectedHost === void 0 ? url.host : extractHost(options.expectedHost);
|
|
100
|
+
const cryptoKey = await importHmacKey(secret);
|
|
101
|
+
const valid = await crypto.subtle.verify(
|
|
102
|
+
"HMAC",
|
|
103
|
+
cryptoKey,
|
|
104
|
+
sigBytes,
|
|
105
|
+
textEncoder.encode(canonicalize(host, key, exp, transform))
|
|
106
|
+
);
|
|
107
|
+
if (!valid) {
|
|
108
|
+
return { reason: "bad_signature", valid: false };
|
|
109
|
+
}
|
|
110
|
+
return { key, transform: transform === "" ? void 0 : transform, valid: true };
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
export { buildSignedImageUrl, verifySignedImageUrl };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
const UPSERT_EMBED_CONCURRENCY = 8;
|
|
2
|
+
const concurrentMap = async (items, limit, function_) => {
|
|
3
|
+
if (items.length === 0) {
|
|
4
|
+
return [];
|
|
5
|
+
}
|
|
6
|
+
const effectiveLimit = Math.max(1, Math.min(limit, items.length));
|
|
7
|
+
const results = Array.from({ length: items.length });
|
|
8
|
+
let cursor = 0;
|
|
9
|
+
const workers = Array.from({ length: effectiveLimit }, async () => {
|
|
10
|
+
for (; ; ) {
|
|
11
|
+
const index = cursor;
|
|
12
|
+
cursor += 1;
|
|
13
|
+
if (index >= items.length) {
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
results[index] = await function_(items[index], index);
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
await Promise.all(workers);
|
|
20
|
+
return results;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export { UPSERT_EMBED_CONCURRENCY as U, concurrentMap as c };
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
const MAX_BLOBS = 20;
|
|
2
|
+
const MAX_DOUBLES = 20;
|
|
3
|
+
const MAX_INDEXES = 1;
|
|
4
|
+
const MAX_BLOB_BYTES = 16 * 1024;
|
|
5
|
+
const MAX_INDEX_BYTES = 96;
|
|
6
|
+
const TEXT_ENCODER = new TextEncoder();
|
|
7
|
+
const byteLengthOf = (value) => {
|
|
8
|
+
if (value === null) {
|
|
9
|
+
return 0;
|
|
10
|
+
}
|
|
11
|
+
if (typeof value === "string") {
|
|
12
|
+
return TEXT_ENCODER.encode(value).length;
|
|
13
|
+
}
|
|
14
|
+
return value.byteLength;
|
|
15
|
+
};
|
|
16
|
+
const assertWithin = (kind, length, max) => {
|
|
17
|
+
if (length > max) {
|
|
18
|
+
throw new RangeError(`@lunora/bindings/analytics: a data point may carry at most ${String(max)} ${kind} (got ${String(length)}).`);
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
const assertByteBudget = (kind, bytes, max) => {
|
|
22
|
+
if (bytes > max) {
|
|
23
|
+
throw new RangeError(`@lunora/bindings/analytics: a data point's ${kind} may total at most ${String(max)} bytes (got ${String(bytes)}).`);
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
const createAnalytics = (binding) => {
|
|
27
|
+
const writeDataPoint = (event) => {
|
|
28
|
+
assertWithin("blobs", event.blobs?.length ?? 0, MAX_BLOBS);
|
|
29
|
+
assertWithin("doubles", event.doubles?.length ?? 0, MAX_DOUBLES);
|
|
30
|
+
assertWithin("indexes", event.indexes?.length ?? 0, MAX_INDEXES);
|
|
31
|
+
const blobBytes = (event.blobs ?? []).reduce((total, blob) => total + byteLengthOf(blob), 0);
|
|
32
|
+
assertByteBudget("blobs", blobBytes, MAX_BLOB_BYTES);
|
|
33
|
+
for (const index of event.indexes ?? []) {
|
|
34
|
+
assertByteBudget("index", byteLengthOf(index), MAX_INDEX_BYTES);
|
|
35
|
+
}
|
|
36
|
+
binding.writeDataPoint(event);
|
|
37
|
+
};
|
|
38
|
+
const track = (name, event = {}) => {
|
|
39
|
+
const dimensionEntries = Object.entries(event.dimensions ?? {});
|
|
40
|
+
const metricEntries = Object.entries(event.metrics ?? {});
|
|
41
|
+
const blobs = [name, ...dimensionEntries.map(([, value]) => value)];
|
|
42
|
+
const doubles = metricEntries.map(([, value]) => value);
|
|
43
|
+
const indexes = event.index === void 0 ? [] : [event.index];
|
|
44
|
+
writeDataPoint({ blobs, doubles, indexes });
|
|
45
|
+
const dimensions = dimensionEntries.map(([field], offset) => {
|
|
46
|
+
return { column: `blob${String(offset + 2)}`, field };
|
|
47
|
+
});
|
|
48
|
+
const metrics = metricEntries.map(([field], offset) => {
|
|
49
|
+
return { column: `double${String(offset + 1)}`, field };
|
|
50
|
+
});
|
|
51
|
+
const index = event.index === void 0 ? null : { column: "index1", field: "index" };
|
|
52
|
+
return { dimensions, index, metrics, name };
|
|
53
|
+
};
|
|
54
|
+
return { track, writeDataPoint };
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export { createAnalytics };
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { c as concurrentMap, U as UPSERT_EMBED_CONCURRENCY } from './concurrent-Dj5sOibv.mjs';
|
|
2
|
+
|
|
3
|
+
const createContextVectors = (lunora) => {
|
|
4
|
+
const upsert = async (indexName, input) => {
|
|
5
|
+
await lunora.upsert(indexName, {
|
|
6
|
+
embed: input.embed,
|
|
7
|
+
id: input.id,
|
|
8
|
+
input: input.input,
|
|
9
|
+
metadata: input.metadata,
|
|
10
|
+
namespace: input.namespace
|
|
11
|
+
});
|
|
12
|
+
};
|
|
13
|
+
return {
|
|
14
|
+
deleteByIds: async (indexName, ids) => {
|
|
15
|
+
await lunora.deleteByIds(indexName, ids);
|
|
16
|
+
},
|
|
17
|
+
getByIds: async (indexName, ids) => {
|
|
18
|
+
const records = await lunora.getByIds(indexName, ids);
|
|
19
|
+
return records.map((record) => {
|
|
20
|
+
return { id: record.id, metadata: record.metadata, values: record.values };
|
|
21
|
+
});
|
|
22
|
+
},
|
|
23
|
+
query: async (indexName, input) => {
|
|
24
|
+
const result = await lunora.query(indexName, {
|
|
25
|
+
embed: input.embed,
|
|
26
|
+
filter: input.filter,
|
|
27
|
+
input: input.input,
|
|
28
|
+
namespace: input.namespace,
|
|
29
|
+
// Default to "indexed" rather than "all": returning every
|
|
30
|
+
// metadata field by default leaks whatever was stored on the
|
|
31
|
+
// vector (potentially cross-tenant if namespaces aren't wired).
|
|
32
|
+
// Callers that need full metadata opt in explicitly via input.
|
|
33
|
+
returnMetadata: input.returnMetadata ?? "indexed",
|
|
34
|
+
topK: input.topK,
|
|
35
|
+
vector: input.vector
|
|
36
|
+
});
|
|
37
|
+
return {
|
|
38
|
+
count: result.count,
|
|
39
|
+
matches: result.matches.map((match) => {
|
|
40
|
+
return { id: match.id, metadata: match.metadata, score: match.score };
|
|
41
|
+
})
|
|
42
|
+
};
|
|
43
|
+
},
|
|
44
|
+
upsert,
|
|
45
|
+
upsertNow: upsert
|
|
46
|
+
};
|
|
47
|
+
};
|
|
48
|
+
const sharedNamespaceWarned = /* @__PURE__ */ new Set();
|
|
49
|
+
const warnSharedNamespace = (indexName) => {
|
|
50
|
+
if (sharedNamespaceWarned.has(indexName)) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
sharedNamespaceWarned.add(indexName);
|
|
54
|
+
console.warn(
|
|
55
|
+
`[@lunora/bindings/vectors] index "${indexName}" syncs vectors without a namespace — in a
|
|
56
|
+
multi-tenant/sharded app this exposes one tenant's vectors (and any captured
|
|
57
|
+
metadata) to every other tenant, since Vectorize indexes are account-global.
|
|
58
|
+
Pass \`namespace\` (the shard/tenant key) on both write and query — query-side
|
|
59
|
+
namespace filtering is mandatory for multi-tenant apps. Single-tenant apps that
|
|
60
|
+
legitimately have no tenant key suppress this via { allowSharedNamespace: true }.`
|
|
61
|
+
);
|
|
62
|
+
};
|
|
63
|
+
const pickMetadata = (row, fields) => {
|
|
64
|
+
const result = {};
|
|
65
|
+
for (const field of fields) {
|
|
66
|
+
if (field in row) {
|
|
67
|
+
result[field] = row[field];
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return result;
|
|
71
|
+
};
|
|
72
|
+
const createVectorSyncHook = (options) => {
|
|
73
|
+
const { allowSharedNamespace, namespace, schema, vectors } = options;
|
|
74
|
+
return async (event) => {
|
|
75
|
+
const tableDefinition = schema.tables[event.table];
|
|
76
|
+
const inlineIndexes = tableDefinition?.vectorIndexes ?? [];
|
|
77
|
+
const standaloneIndexes = Object.entries(schema.vectorIndexes).filter(([, definition]) => definition.table === event.table);
|
|
78
|
+
if (inlineIndexes.length === 0 && standaloneIndexes.length === 0) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
const allIndexNames = [...inlineIndexes.map((index) => index.name), ...standaloneIndexes.map(([name]) => name)];
|
|
82
|
+
if (event.op === "delete") {
|
|
83
|
+
await Promise.all(allIndexNames.map((name) => vectors.deleteByIds(name, [event.id])));
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
const row = event.doc;
|
|
87
|
+
if (!row) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const inlineWithValue = inlineIndexes.map((index) => {
|
|
91
|
+
return { index, value: row[index.field] };
|
|
92
|
+
});
|
|
93
|
+
const inlineToUpsert = inlineWithValue.filter((entry) => entry.value !== void 0 && entry.value !== null);
|
|
94
|
+
const inlineToClear = inlineWithValue.filter((entry) => entry.value === void 0 || entry.value === null);
|
|
95
|
+
for (const { index, value } of inlineToUpsert) {
|
|
96
|
+
if (typeof value !== "string") {
|
|
97
|
+
throw new TypeError(
|
|
98
|
+
`@lunora/bindings/vectors: inline index "${index.name}" expects a string source at "${index.field}" on table "${event.table}" (got ${typeof value}); use a standalone defineVectorIndex with a select() to derive text from non-string columns`
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
const operations = [
|
|
103
|
+
...inlineToClear.map((entry) => async () => {
|
|
104
|
+
await vectors.deleteByIds(entry.index.name, [event.id]);
|
|
105
|
+
}),
|
|
106
|
+
...inlineToUpsert.map((entry) => async () => {
|
|
107
|
+
if (!allowSharedNamespace && namespace === void 0) {
|
|
108
|
+
warnSharedNamespace(entry.index.name);
|
|
109
|
+
}
|
|
110
|
+
await vectors.upsert(entry.index.name, {
|
|
111
|
+
embed: entry.index.embed,
|
|
112
|
+
id: event.id,
|
|
113
|
+
input: entry.value,
|
|
114
|
+
metadata: entry.index.metadata ? pickMetadata(row, entry.index.metadata) : void 0,
|
|
115
|
+
namespace
|
|
116
|
+
});
|
|
117
|
+
}),
|
|
118
|
+
...standaloneIndexes.map(([name, definition]) => async () => {
|
|
119
|
+
if (!allowSharedNamespace && namespace === void 0) {
|
|
120
|
+
warnSharedNamespace(name);
|
|
121
|
+
}
|
|
122
|
+
await vectors.upsert(name, {
|
|
123
|
+
embed: definition.embed,
|
|
124
|
+
id: event.id,
|
|
125
|
+
input: definition.select(row),
|
|
126
|
+
metadata: definition.metadata?.(row),
|
|
127
|
+
namespace
|
|
128
|
+
});
|
|
129
|
+
})
|
|
130
|
+
];
|
|
131
|
+
try {
|
|
132
|
+
await concurrentMap(operations, UPSERT_EMBED_CONCURRENCY, async (operation) => operation());
|
|
133
|
+
} catch (error) {
|
|
134
|
+
await Promise.allSettled(allIndexNames.map((name) => vectors.deleteByIds(name, [event.id])));
|
|
135
|
+
throw error;
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
export { createContextVectors, createVectorSyncHook };
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
const ALLOWED_OUTPUT_FORMATS = /* @__PURE__ */ new Set(["image/avif", "image/gif", "image/jpeg", "image/png", "image/webp"]);
|
|
2
|
+
const DEFAULT_MAX_DIMENSION = 1e4;
|
|
3
|
+
const DEFAULT_OUTPUT_FORMAT = "image/webp";
|
|
4
|
+
const isR2ObjectBody = (input) => {
|
|
5
|
+
if (typeof input !== "object") {
|
|
6
|
+
return false;
|
|
7
|
+
}
|
|
8
|
+
const object = input;
|
|
9
|
+
return "body" in object && !(input instanceof ArrayBuffer) && !(input instanceof Uint8Array);
|
|
10
|
+
};
|
|
11
|
+
const toStream = (input) => {
|
|
12
|
+
if (input instanceof ReadableStream) {
|
|
13
|
+
return input;
|
|
14
|
+
}
|
|
15
|
+
if (isR2ObjectBody(input)) {
|
|
16
|
+
if (input.body === null) {
|
|
17
|
+
throw new Error("@lunora/bindings/images: R2 object body is null (object missing or already consumed)");
|
|
18
|
+
}
|
|
19
|
+
return input.body;
|
|
20
|
+
}
|
|
21
|
+
if (input instanceof Blob) {
|
|
22
|
+
return input.stream();
|
|
23
|
+
}
|
|
24
|
+
const bytes = input instanceof Uint8Array ? input : new Uint8Array(input);
|
|
25
|
+
return new ReadableStream({
|
|
26
|
+
start(controller) {
|
|
27
|
+
controller.enqueue(bytes);
|
|
28
|
+
controller.close();
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
};
|
|
32
|
+
const sanitizeTransform = (transform, maxDimension) => {
|
|
33
|
+
if (transform === void 0) {
|
|
34
|
+
return {};
|
|
35
|
+
}
|
|
36
|
+
const clampDimension = (value) => {
|
|
37
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
38
|
+
throw new Error("@lunora/bindings/images: width/height must be a positive finite number");
|
|
39
|
+
}
|
|
40
|
+
return Math.min(Math.floor(value), maxDimension);
|
|
41
|
+
};
|
|
42
|
+
const rest = { ...transform };
|
|
43
|
+
delete rest.draw;
|
|
44
|
+
return {
|
|
45
|
+
...rest,
|
|
46
|
+
...transform.width === void 0 ? {} : { width: clampDimension(transform.width) },
|
|
47
|
+
...transform.height === void 0 ? {} : { height: clampDimension(transform.height) }
|
|
48
|
+
};
|
|
49
|
+
};
|
|
50
|
+
const splitOverlay = (overlay) => {
|
|
51
|
+
const { image, transform, ...drawOptions } = overlay;
|
|
52
|
+
return { drawOptions, image, transform };
|
|
53
|
+
};
|
|
54
|
+
const resolveOutput = (output) => {
|
|
55
|
+
const format = output?.format ?? DEFAULT_OUTPUT_FORMAT;
|
|
56
|
+
if (!ALLOWED_OUTPUT_FORMATS.has(format)) {
|
|
57
|
+
throw new Error(`@lunora/bindings/images: unsupported output format "${format}" (allowed: ${[...ALLOWED_OUTPUT_FORMATS].join(", ")})`);
|
|
58
|
+
}
|
|
59
|
+
return { ...output, format };
|
|
60
|
+
};
|
|
61
|
+
const createImages = (options) => {
|
|
62
|
+
const { binding } = options;
|
|
63
|
+
const maxDimension = options.maxDimension ?? DEFAULT_MAX_DIMENSION;
|
|
64
|
+
return {
|
|
65
|
+
info: async (input) => binding.info(toStream(input)),
|
|
66
|
+
transform: async (input, transform, output, overlays) => {
|
|
67
|
+
const safeTransform = sanitizeTransform(transform, maxDimension);
|
|
68
|
+
const outputOptions = resolveOutput(output);
|
|
69
|
+
let transformer = binding.input(toStream(input)).transform(safeTransform);
|
|
70
|
+
for (const overlay of overlays ?? []) {
|
|
71
|
+
const { drawOptions, image, transform: overlayTransform } = splitOverlay(overlay);
|
|
72
|
+
const overlayImage = overlayTransform === void 0 ? toStream(image) : binding.input(toStream(image)).transform(sanitizeTransform(overlayTransform, maxDimension));
|
|
73
|
+
transformer = transformer.draw(overlayImage, drawOptions);
|
|
74
|
+
}
|
|
75
|
+
return transformer.output(outputOptions);
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export { createImages };
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
const MAX_KEY_LENGTH = 512;
|
|
2
|
+
const MAX_LIST_LIMIT = 1e3;
|
|
3
|
+
const validateKey = (key) => {
|
|
4
|
+
if (typeof key !== "string" || key.length === 0) {
|
|
5
|
+
throw new Error("@lunora/bindings/kv: key must be a non-empty string");
|
|
6
|
+
}
|
|
7
|
+
if (key.length > MAX_KEY_LENGTH) {
|
|
8
|
+
throw new Error(`@lunora/bindings/kv: key exceeds ${String(MAX_KEY_LENGTH)}-byte limit`);
|
|
9
|
+
}
|
|
10
|
+
if (key.includes("\0")) {
|
|
11
|
+
throw new Error("@lunora/bindings/kv: key contains NUL byte");
|
|
12
|
+
}
|
|
13
|
+
const segments = key.split("/");
|
|
14
|
+
for (const segment of segments) {
|
|
15
|
+
if (segment === "." || segment === "..") {
|
|
16
|
+
throw new Error("@lunora/bindings/kv: key contains a `.`/`..` path component");
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
const validatePrefix = (prefix) => {
|
|
21
|
+
if (prefix.length === 0) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
if (prefix.length > MAX_KEY_LENGTH) {
|
|
25
|
+
throw new Error(`@lunora/bindings/kv: prefix exceeds ${String(MAX_KEY_LENGTH)}-byte limit`);
|
|
26
|
+
}
|
|
27
|
+
if (prefix.includes("\0")) {
|
|
28
|
+
throw new Error("@lunora/bindings/kv: prefix contains NUL byte");
|
|
29
|
+
}
|
|
30
|
+
const segments = prefix.split("/");
|
|
31
|
+
for (const segment of segments) {
|
|
32
|
+
if (segment === "." || segment === "..") {
|
|
33
|
+
throw new Error("@lunora/bindings/kv: prefix contains a `.`/`..` path component");
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
const scopeKey = (prefix, key) => {
|
|
38
|
+
validateKey(prefix);
|
|
39
|
+
validateKey(key);
|
|
40
|
+
const trimmedPrefix = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
|
|
41
|
+
const composed = `${trimmedPrefix}/${key}`;
|
|
42
|
+
if (composed.length > MAX_KEY_LENGTH) {
|
|
43
|
+
throw new Error(`@lunora/bindings/kv: scoped key exceeds ${String(MAX_KEY_LENGTH)}-byte limit`);
|
|
44
|
+
}
|
|
45
|
+
return composed;
|
|
46
|
+
};
|
|
47
|
+
const toPutOptions = (options) => {
|
|
48
|
+
const out = {};
|
|
49
|
+
if (options.expiration !== void 0 && options.expirationTtl !== void 0) {
|
|
50
|
+
throw new Error("@lunora/bindings/kv: `expiration` and `expirationTtl` are mutually exclusive");
|
|
51
|
+
}
|
|
52
|
+
if (options.expiration !== void 0) {
|
|
53
|
+
out.expiration = options.expiration;
|
|
54
|
+
}
|
|
55
|
+
if (options.expirationTtl !== void 0) {
|
|
56
|
+
out.expirationTtl = options.expirationTtl;
|
|
57
|
+
}
|
|
58
|
+
if (options.metadata !== void 0) {
|
|
59
|
+
out.metadata = options.metadata;
|
|
60
|
+
}
|
|
61
|
+
return Object.keys(out).length > 0 ? out : void 0;
|
|
62
|
+
};
|
|
63
|
+
const createKv = (options) => {
|
|
64
|
+
if (!options.namespace) {
|
|
65
|
+
throw new Error("@lunora/bindings/kv: `namespace` is required");
|
|
66
|
+
}
|
|
67
|
+
const { keyPrefix, namespace } = options;
|
|
68
|
+
if (keyPrefix !== void 0) {
|
|
69
|
+
validateKey(keyPrefix);
|
|
70
|
+
}
|
|
71
|
+
const resolve = (key) => {
|
|
72
|
+
if (keyPrefix === void 0) {
|
|
73
|
+
validateKey(key);
|
|
74
|
+
return key;
|
|
75
|
+
}
|
|
76
|
+
return scopeKey(keyPrefix, key);
|
|
77
|
+
};
|
|
78
|
+
const stripPrefix = (name) => {
|
|
79
|
+
if (keyPrefix === void 0) {
|
|
80
|
+
return name;
|
|
81
|
+
}
|
|
82
|
+
const trimmed = keyPrefix.endsWith("/") ? keyPrefix : `${keyPrefix}/`;
|
|
83
|
+
return name.startsWith(trimmed) ? name.slice(trimmed.length) : name;
|
|
84
|
+
};
|
|
85
|
+
const get = async (key, getOptions = {}) => {
|
|
86
|
+
const value = await namespace.get(resolve(key), { cacheTtl: getOptions.cacheTtl, type: "json" });
|
|
87
|
+
return value ?? null;
|
|
88
|
+
};
|
|
89
|
+
const getRaw = async (key, getOptions = {}) => {
|
|
90
|
+
const value = await namespace.get(resolve(key), { cacheTtl: getOptions.cacheTtl, type: getOptions.type ?? "text" });
|
|
91
|
+
return value ?? null;
|
|
92
|
+
};
|
|
93
|
+
const getWithMetadata = async (key, getOptions = {}) => {
|
|
94
|
+
const result = await namespace.getWithMetadata(resolve(key), { cacheTtl: getOptions.cacheTtl, type: "json" });
|
|
95
|
+
return { metadata: result.metadata ?? null, value: result.value ?? null };
|
|
96
|
+
};
|
|
97
|
+
const put = async (key, value, putOptions = {}) => {
|
|
98
|
+
const body = putOptions.raw ? value : JSON.stringify(value);
|
|
99
|
+
await namespace.put(resolve(key), body, toPutOptions(putOptions));
|
|
100
|
+
};
|
|
101
|
+
const deleteKey = async (key) => {
|
|
102
|
+
await namespace.delete(resolve(key));
|
|
103
|
+
};
|
|
104
|
+
const list = async (listOptions = {}) => {
|
|
105
|
+
if (listOptions.limit !== void 0 && (!Number.isInteger(listOptions.limit) || listOptions.limit <= 0)) {
|
|
106
|
+
throw new Error("@lunora/bindings/kv: `limit` must be a positive integer");
|
|
107
|
+
}
|
|
108
|
+
let { prefix } = listOptions;
|
|
109
|
+
if (prefix !== void 0) {
|
|
110
|
+
validatePrefix(prefix);
|
|
111
|
+
}
|
|
112
|
+
if (keyPrefix !== void 0) {
|
|
113
|
+
const base = keyPrefix.endsWith("/") ? keyPrefix : `${keyPrefix}/`;
|
|
114
|
+
prefix = listOptions.prefix === void 0 ? base : `${base}${listOptions.prefix}`;
|
|
115
|
+
validatePrefix(prefix);
|
|
116
|
+
}
|
|
117
|
+
const limit = listOptions.limit === void 0 ? void 0 : Math.min(listOptions.limit, MAX_LIST_LIMIT);
|
|
118
|
+
const result = await namespace.list({ cursor: listOptions.cursor, limit, prefix });
|
|
119
|
+
const keys = result.keys.map((entry) => {
|
|
120
|
+
return {
|
|
121
|
+
...entry,
|
|
122
|
+
name: stripPrefix(entry.name)
|
|
123
|
+
};
|
|
124
|
+
});
|
|
125
|
+
return {
|
|
126
|
+
cursor: result.list_complete ? void 0 : result.cursor,
|
|
127
|
+
keys,
|
|
128
|
+
listComplete: result.list_complete
|
|
129
|
+
};
|
|
130
|
+
};
|
|
131
|
+
return {
|
|
132
|
+
delete: deleteKey,
|
|
133
|
+
get,
|
|
134
|
+
getRaw,
|
|
135
|
+
getWithMetadata,
|
|
136
|
+
list,
|
|
137
|
+
put
|
|
138
|
+
};
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
export { createKv, scopeKey };
|