Skip to content

Commit 1e6a1fc

Browse files
authored
Merge pull request #423 from outerbase/deserialize-cf-kv
feat: deserialize _cf_kv value
2 parents 73ffae9 + 8fbf0d6 commit 1e6a1fc

File tree

8 files changed

+583
-42
lines changed

8 files changed

+583
-42
lines changed

.vscode/settings.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,6 @@
55
"editor.tabSize": 2,
66
"editor.codeActionsOnSave": {
77
"source.organizeImports": "explicit"
8-
}
8+
},
9+
"jest.runMode": "on-demand"
910
}

src/components/gui/schema-sidebar-list.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,11 @@ import { scc } from "@/core/command";
55
import { DatabaseSchemaItem } from "@/drivers/base-driver";
66
import { triggerEditorExtensionTab } from "@/extensions/trigger-editor";
77
import { ExportFormat, exportTableData } from "@/lib/export-helper";
8-
import { Table } from "@phosphor-icons/react";
8+
import { Icon, Table } from "@phosphor-icons/react";
99
import { LucideCog, LucideDatabase, LucideView } from "lucide-react";
1010
import { useCallback, useEffect, useMemo, useState } from "react";
1111
import { ListView, ListViewItem } from "../listview";
12+
import { CloudflareIcon } from "../resource-card/icon";
1213
import SchemaCreateDialog from "./schema-editor/schema-create";
1314

1415
interface SchemaListProps {
@@ -39,12 +40,17 @@ function prepareListViewItem(
3940
let icon = Table;
4041
let iconClassName = "";
4142

43+
console.log("ss", s);
44+
4245
if (s.type === "trigger") {
4346
icon = LucideCog;
4447
iconClassName = "text-purple-500";
4548
} else if (s.type === "view") {
4649
icon = LucideView;
4750
iconClassName = "text-green-600 dark:text-green-300";
51+
} else if (s.type === "table" && s.name === "_cf_KV") {
52+
icon = CloudflareIcon as Icon;
53+
iconClassName = "text-orange-500";
4854
}
4955

5056
return {

src/components/gui/table-result/render-cell.tsx

Lines changed: 85 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import BlobCell from "@/components/gui/table-cell/blob-cell";
22
import { DatabaseValue } from "@/drivers/base-driver";
33
import parseSafeJson from "@/lib/json-safe";
4+
import { deserializeV8 } from "@/lib/v8-derialization";
45
import { ColumnType } from "@outerbase/sdk-transform";
6+
import { useMemo } from "react";
57
import BigNumberCell from "../table-cell/big-number-cell";
68
import GenericCell from "../table-cell/generic-cell";
79
import NumberCell from "../table-cell/number-cell";
@@ -42,17 +44,93 @@ function determineCellType(value: unknown) {
4244
return undefined;
4345
}
4446

45-
export default function tableResultCellRenderer({
46-
y,
47-
x,
48-
state,
49-
header,
50-
isFocus,
51-
}: OptimizeTableCellRenderProps<TableHeaderMetadata>) {
47+
function CloudflareKvValue({
48+
props,
49+
}: {
50+
props: OptimizeTableCellRenderProps<TableHeaderMetadata>;
51+
}) {
52+
const { y, x, state, header, isFocus } = props;
53+
54+
const value = useMemo(() => {
55+
const rawBuffer = state.getValue(y, x);
56+
let buffer = new ArrayBuffer();
57+
58+
if (rawBuffer instanceof ArrayBuffer) {
59+
buffer = rawBuffer;
60+
} else if (rawBuffer instanceof Uint8Array) {
61+
buffer = rawBuffer.buffer as ArrayBuffer;
62+
} else if (rawBuffer instanceof Array) {
63+
buffer = new Uint8Array(rawBuffer).buffer;
64+
}
65+
66+
return deserializeV8(buffer);
67+
}, [y, x, state]);
68+
69+
let displayValue: string | null = "";
70+
71+
if (value.value !== undefined) {
72+
if (typeof value.value === "string") {
73+
displayValue = value.value;
74+
} else if (value.value === null) {
75+
displayValue = null;
76+
} else if (typeof value.value === "object") {
77+
// Protect from circular references
78+
try {
79+
displayValue = JSON.stringify(value.value, null);
80+
} catch (e) {
81+
if (e instanceof Error) {
82+
value.error = e.message;
83+
} else {
84+
value.error = String(e);
85+
}
86+
}
87+
} else {
88+
displayValue = String(value.value);
89+
}
90+
}
91+
92+
if (value.error) {
93+
return (
94+
<div className="h-[35px] px-2 font-mono leading-[35px] text-red-500!">
95+
Error: {value.error}
96+
</div>
97+
);
98+
}
99+
100+
return (
101+
<TextCell
102+
header={header}
103+
state={state}
104+
editor={detectTextEditorType(displayValue)}
105+
editMode={false}
106+
value={displayValue}
107+
valueType={ColumnType.TEXT}
108+
focus={isFocus}
109+
onChange={(newValue) => {
110+
state.changeValue(y, x, newValue);
111+
}}
112+
/>
113+
);
114+
}
115+
116+
export default function tableResultCellRenderer(
117+
props: OptimizeTableCellRenderProps<TableHeaderMetadata>
118+
) {
119+
const { y, x, state, header, isFocus } = props;
120+
52121
const editMode = isFocus && state.isInEditMode();
53122
const value = state.getValue(y, x);
123+
54124
const valueType = determineCellType(value);
55125

126+
// Check if it is Cloudflare KV type
127+
if (
128+
header.metadata?.from?.table === "_cf_KV" &&
129+
header.metadata?.from?.column === "value"
130+
) {
131+
return <CloudflareKvValue props={props} />;
132+
}
133+
56134
switch (valueType ?? header.metadata.type) {
57135
case ColumnType.INTEGER:
58136
return (

src/components/listview/index.tsx

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,8 @@ import {
66
} from "@/components/ui/context-menu";
77
import { OpenContextMenuList } from "@/core/channel-builtin";
88
import { cn } from "@/lib/utils";
9-
import {
10-
LucideChevronDown,
11-
LucideChevronRight,
12-
LucideIcon,
13-
} from "lucide-react";
9+
import { Icon } from "@phosphor-icons/react";
10+
import { LucideChevronDown, LucideChevronRight } from "lucide-react";
1411
import React, {
1512
Dispatch,
1613
Fragment,
@@ -24,7 +21,7 @@ import HighlightText from "../ui/highlight-text";
2421
export interface ListViewItem<T = unknown> {
2522
key: string;
2623
name: string;
27-
icon: LucideIcon;
24+
icon: Icon;
2825
iconColor?: string;
2926
iconBadgeColor?: string;
3027
data: T;

src/lib/build-table-result.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,16 @@ export function pipeVirtualColumnAsReadOnly(
207207
}
208208
}
209209

210+
export function pipeCloudflareSpecialTable(
211+
headers: OptimizeTableHeaderProps<TableHeaderMetadata>[]
212+
) {
213+
for (const header of headers) {
214+
if (header.metadata.from?.table === "_cf_KV") {
215+
header.setting.readonly = true;
216+
}
217+
}
218+
}
219+
210220
export function pipeCalculateInitialSize(
211221
headers: OptimizeTableHeaderProps<TableHeaderMetadata>[],
212222
{ result }: BuildTableResultProps
@@ -293,6 +303,7 @@ export function buildTableResultHeader(
293303
pipeAttachColumnViaSchemas(headers, props);
294304
pipeEditableTable(headers, props);
295305
pipeVirtualColumnAsReadOnly(headers);
306+
pipeCloudflareSpecialTable(headers);
296307
pipeCalculateInitialSize(headers, props);
297308
pipeColumnIcon(headers);
298309

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { deserializeV8 } from ".";
2+
3+
function p(hex: string) {
4+
const buffer = new Uint8Array(hex.length / 2);
5+
for (let i = 0; i < hex.length; i += 2) {
6+
buffer[i / 2] = parseInt(hex.substring(i, i + 2), 16);
7+
}
8+
9+
return deserializeV8(buffer.buffer);
10+
}
11+
12+
describe("V8 Deserialization", () => {
13+
it("positive small number", () => {
14+
expect(p("FF0F4906").value).toBe(3);
15+
});
16+
17+
it("negative number", () => {
18+
expect(p("FF0F4905").value).toBe(-3);
19+
});
20+
21+
it("positive large number", () => {
22+
expect(p("FF0F4994B0BEDF01").value).toBe(234343434);
23+
});
24+
25+
it("string", () => {
26+
expect(p("FF0F220B68656C6C6F20776F726C64").value).toBe("hello world");
27+
});
28+
29+
it("unicode string", () => {
30+
expect(p("FF0F6308604F7D59164E4C75").value).toBe("你好世界");
31+
});
32+
33+
it("true", () => {
34+
expect(p("FF0F54").value).toBe(true);
35+
});
36+
37+
it("false", () => {
38+
expect(p("FF0F46").value).toBe(false);
39+
});
40+
41+
it("null", () => {
42+
expect(p("FF0F30").value).toBe(null);
43+
});
44+
45+
it("double", () => {
46+
expect(p("FF0F4E1F85EB51B81E0940").value).toBeCloseTo(3.14);
47+
});
48+
49+
it("big number", () => {
50+
expect(p("FF0F5A10D20A1FEB8CA954AB").value).toBe(12345678901234567890n);
51+
});
52+
53+
it("array", () => {
54+
expect(p("FF0F4103220568656C6C6F2205776F726C64490A240003").value).toEqual([
55+
"hello",
56+
"world",
57+
5,
58+
]);
59+
});
60+
61+
it("object", () => {
62+
expect(
63+
p("FF0F6F220568656C6C6F2205776F726C6422066E756D626572490A7B02").value
64+
).toEqual({ hello: "world", number: 5 });
65+
});
66+
67+
it("object with undefined", () => {
68+
expect(
69+
p(
70+
"FF0F6F220568656C6C6F2205776F726C6422036172724103490249044906240003220275645F7B03"
71+
).value
72+
).toEqual({ hello: "world", arr: [1, 2, 3], ud: undefined });
73+
});
74+
75+
it("date", () => {
76+
const date = new Date(1743508780000);
77+
expect(p("FF0F4400003E8B135F7942").value).toEqual(date);
78+
});
79+
});

0 commit comments

Comments
 (0)