Inspect forge Storage API

Hi,

is there a way to inspect what is inside my forge storage API store for my dev env at moment?

So I can delete one key I don’t need more in forte for example?

Thank you

Hi @bongartz ,

Forge doesn’t have an out of the box mechanism yet, but you could build it into an app. Here’s some code to get you started. To enable it, you’ll need to set the environment storageBrowserEnabled to true as shown in the code.

import ForgeUI, {
  Button,
  render,
  Fragment,
  Macro,
  Text,
  useAction,
  useState,
  Table,
  Head,
  Cell,
  Row,
  ButtonSet
} from "@forge/ui";
import { storage, startsWith, QueryBuilder, ListResult, Result, store } from '@forge/api';

const itemsToCreatePerRequest = 15;
const itemsPerQuery = 10;

const App = () => {

  const createData = async (): Promise<Result[]> => {
    const createdItems: Result[] = [];
    for (let i = 0; i < itemsToCreatePerRequest; i++) {
      const item: Result = {
        key: `foo-${new Date().getTime()}-${i}`,
        value: new Date()
      }
      storage.set(item.key, item.value);
    }
    return createdItems
  }

  const getData = async (cursor: any): Promise<ListResult> => {
    let query: QueryBuilder = storage.query().limit(itemsPerQuery);
    if (cursor) {
      query = query.cursor(cursor)
    }
    return await query.getMany();
  }

  const getMoreData = async (): Promise<ListResult> => {
    return getData(lastResult.nextCursor);
  }

  const findDataToDelete = async (cursor: any, itemKeysToDelete: string[]): Promise<void> => {
    const data = await getData(cursor);
    if (data.results.length) {
      data.results.forEach(async (item) => {
        itemKeysToDelete.push(item.key);
      });
      if (data.nextCursor) {
        await findDataToDelete(data.nextCursor, itemKeysToDelete);
      }
    }
  }

  const deleteItems = async (itemKeysToDelete: string[]): Promise<void> => {
    const keysToRetry: string[] = [];
    const deletionPromises: Promise<any>[] = itemKeysToDelete.map(async (itemKey) => {
      try {
        return await storage.delete(itemKey);
      } catch {
        keysToRetry.push(itemKey);
      }
    });
    await Promise.all(deletionPromises);
    if (keysToRetry.length) {
      return new Promise((resolve, reject) => {
        setTimeout(async () => {
          await deleteItems(keysToRetry);
          resolve();
        }, 1000);
      });
    }
  }

  const deleteAllData = async (cursor: any): Promise<void> => {
    const itemKeysToDelete: string[] = [];
    await findDataToDelete(undefined, itemKeysToDelete);
    await deleteItems(itemKeysToDelete);
  }

  // Enable this UI with the following commands:
  // forge variables:set storageBrowserEnabled true
  // export FORGE_USER_VAR_storageBrowserEnabled=true
  const enabled = process.env.storageBrowserEnabled;

  const [initialData] = useAction(value => value, async () => await getData(undefined));
  const [lastResult, setLastResult] = useState<undefined | ListResult>(initialData);

  const renderCreateDataButton = () => {
    return (
      <Button
        text="Create data"
        onClick={async () => {
          await createData();
        }}
      />
    );
  }

  const renderGetMoreDataButton = () => {
    return (
      <Button
        text="Get more data"
        onClick={async () => {
          const data: ListResult = await getMoreData();
          const allItems = lastResult && lastResult.results && lastResult.results.length ? lastResult.results : [];
          if (data.results && data.results.length) {
            data.results.forEach(item => {
              allItems.push(item);
            });
          }
          data.results = allItems;
          setLastResult(data);
        }}
      />
    );
  }

  const renderDeleteAllItemsButton = () => {
    return (
      <Button
        text="Delete all data"
        onClick={async () => {
          await deleteAllData(undefined);
          const data: ListResult = {
            results: [],
            nextCursor: undefined
          };
          setLastResult(data);
        }}
      />
    );
  }

  const renderDeleteItemButton = (itemToDelete: Result) => {
    // It's possible that deleting an item may invalidate the cursor so perhaps it should be
    // reset rather than keeping it as done here.
    return (
      <Button
        text="Delete item"
        onClick={async () => {
          await storage.delete(itemToDelete.key);
          const allItems: Result[] = [];
          if (lastResult.results) {
            for (const item of lastResult.results) {
              if (item.key !== itemToDelete.key) {
                allItems.push(item);
              }
            }
          }
          const data: ListResult = {
            results: allItems,
            nextCursor: lastResult.nextCursor
          };
          setLastResult(data);
        }}
      />
    );
  }

  const renderData = () => {
    return (
      <Table>
        <Head>
          <Cell>
            <Text>Key</Text>
          </Cell>
          <Cell>
            <Text>Data</Text>
          </Cell>
          <Cell>
            <Text>Operations</Text>
          </Cell>
        </Head>
        {lastResult && lastResult.results ? lastResult.results.map(item => (
          <Row>
            <Cell>
              <Text>{item.key}</Text>
            </Cell>
            <Cell>
              <Text>{`${item.value}`}</Text>
            </Cell>
            <Cell>
              {renderDeleteItemButton(item)}
            </Cell>
          </Row>
        )) : (
          <Row>
            <Cell>
              <Text>-</Text>
            </Cell>
            <Cell>
              <Text>-</Text>
            </Cell>
            <Cell>
              <Text>-</Text>
            </Cell>
          </Row>
        )}
      </Table>
    );
  }

  const renderBrowseDataUi = () => {
    return (
      <Fragment>
        <Text>Forge Storage Inspector</Text>
        <ButtonSet>
          {renderCreateDataButton()}
          {renderGetMoreDataButton()}
          {renderDeleteAllItemsButton()}
        </ButtonSet>
        {renderData()}
      </Fragment>
    );
  }

return enabled ? renderBrowseDataUi() : null;
};

export const run = render(
  <Macro
    app={<App />}
  />
);

Regards,
Dugald

5 Likes

Sweet, Dugald! I actually thought of writing something like this as a little NPM package for Codegeist but didn’t find the time to participate. Wouldn’t it be great if Forge supported something like this out of the box? :slight_smile:

Cheers,
Sven

3 Likes

@SvenSchatter I think yes this is a helpfull tool.

All other databases have a small app or a console for inspecting there data.