GraphDB Kuzu(WebAssembly)でデータをlocalStorageに読み書きしたメモ

# 概要
[前回](https://qiita.com/hibohiboo/items/30aadc02d932e579e8c2)はkuzuを動作させるところまで確認した。
今回はローカルストレージへの永続化、復元を試してみた

[この時点のソース](https://github.com/hibohiboo/kartagraph-monorepo/blob/3f19a46c9daf742da82672d8730f11571e5e079d/apps/kuzu/src/App.tsx)

# ソースコード

今回はグラフ描画に[Cytoscape](https://js.cytoscape.org/)を試している

```tsx
import kuzu_wasm, { Connection, Kuzu } from '@kuzu/kuzu-wasm';
import Cytoscape from 'cytoscape';
import { useEffect, useRef, useState } from 'react';
import { CytoscapeComponent } from './shared/ui';

function App() {
  const [elements, setNElements] = useState<{ data: { id?: string; source?: string; target?: string; label?: string } }[]>([]);
  const cyref = useRef<Cytoscape.CoreLayout | null>(null);
  const conRef = useRef<Connection | null>(null);
  const kuzuRef = useRef<Kuzu | null>(null);
  const draw = async () => {
    if (!conRef.current) return;
    // Execute Cypher query
    const response = await conRef.current.execute(
      `
   MATCH (a:User)
   RETURN *;
   `,
    );
    const users = !response.table ? [] : JSON.parse(response.table.toString());
    type ID = { offset: string; table: string };
    const getId = (id: ID): string => `id_${id.table}_${id.offset}`;
    const nodes = users.map(({ a }: { a: { _ID: ID; name: string } }) => ({ data: { id: getId(a._ID), label: a.name } }));

    const relsResponse = await conRef.current.execute(
      `
   MATCH (a:User)-[r:Follows]->(b:User)
   RETURN *;
   `,
    );
    const rels = !relsResponse.table ? [] : JSON.parse(relsResponse.table.toString());

    const relsData = rels.map(
      ({ r }: { a: { _ID: ID; name: string }; r: { _ID: ID; _SRC: ID; _DST: ID; since: string }; b: { _ID: ID; name: string } }) => ({
        data: {
          source: getId(r._SRC),
          target: getId(r._DST),
          id: getId(r._ID),
          label: r.since,
        },
      }),
    );
    setNElements([...nodes, ...relsData]);
    setTimeout(() => {
      if (cyref.current) cyref.current.layout({ name: 'grid' }).run();
    }, 0);
  };
  useEffect(() => {
    (async () => {
      const kuzu = await kuzu_wasm();
      const db = await kuzu.Database();
      const conn = await kuzu.Connection(db);
      conRef.current = conn;
      kuzuRef.current = kuzu;

      kuzu.FS.writeFile('/follows.csv', await (await fetch('/data/follows.csv')).text());
      kuzu.FS.writeFile('/user.csv', await (await fetch('/data/user.csv')).text());

      // Create schema
      await conn.execute('CREATE NODE TABLE User(name STRING, age INT64, PRIMARY KEY (name))');
      await conn.execute('CREATE REL TABLE Follows(FROM User TO User, since INT64)');

      // Insert data
      await conn.execute('COPY User FROM "/user.csv"');
      await conn.execute('COPY Follows FROM "/follows.csv"');

      // Execute Cypher query
      await draw();
    })();
  }, []);
  return (
    <>
      <CytoscapeComponent
        elements={elements}
        cy={(cy) => {
          cyref.current = cy;
        }}
        layout={{ name: 'grid' }}
        wheelSensitivity={0.1}
        style={{ width: '600px', height: '600px' }}
      />
      <form
        onSubmit={async (e) => {
          e.preventDefault();
          console.log(e);
          if (!conRef.current) return;
          const form = new FormData(e.currentTarget);
          const userName = form.get('username') || '';
          if (!userName) return;
          await conRef.current.execute(`CREATE (u:User {name: '${userName}', age: 10});`);
          await draw();
          return;
        }}
      >
        <input type="text" name="username" />
        <button type="submit">ユーザ追加</button>
      </form>
      <button
        onClick={async () => {
          if (!conRef.current || !kuzuRef.current) return;
          await conRef.current.execute(`COPY (MATCH (u:User) RETURN u.*) TO '/bakcup.csv' (header=false);`);

          const contents = kuzuRef.current.FS.readFile('/bakcup.csv', { encoding: 'utf8' });
          console.log('contents', contents);
          localStorage.setItem('backup_graphdb_user', contents);
        }}
      >
        保存
      </button>
      <button
        onClick={async () => {
          if (!conRef.current || !kuzuRef.current) return;
          console.log('load', localStorage.getItem('backup_graphdb_user'));
          kuzuRef.current.FS.writeFile('/storage.csv', localStorage.getItem('backup_graphdb_user') || '');
          // 一度消してからでないとエラーになる
          await conRef.current.execute(`MATCH (u:User) DETACH DELETE u`);
          await conRef.current.execute('COPY User FROM "/storage.csv"');
          await conRef.current.execute('COPY Follows FROM "/follows.csv"');
          await draw();
        }}
      >
        復元
      </button>
    </>
  );
}

export default App;

```

# 参考
[copy to csv](https://docs.kuzudb.com/export/csv/) 
[what-is-kuzu-wasm](https://github.com/unswdb/kuzu-wasm/blob/main/docs/guide/what-is-kuzu-wasm.md)
[VFS](https://github.com/unswdb/kuzu-wasm/blob/main/docs/guide/data-ingestion-overview.md)
[VFS - readfile](https://emscripten.org/docs/api_reference/Filesystem-API.html#FS.readFile)

[アップロードサンプル](https://github.com/unswdb/kuzu-wasm/blob/main/docs/guide/fetch-from-network.md)
[write](https://github.com/unswdb/kuzu-wasm/blob/main/docs/guide/writing-to-file.md)
[migrate](https://docs.kuzudb.com/migrate/)
[node.js api](https://kuzudb.com/api-docs/nodejs/)