Last updated Apr 16, 2024

Use custom entities to store structured data

This tutorial uses UI Kit 1 and @forge/cli version 7.1.0. This tutorial won't work if you're using the latest version of @forge/cli.

This tutorial demonstrates how to store structured data through custom entities and query that data. This will help you understand how custom entities work in Forge.

You’ll build a Confluence app that can create users with multiple attributes (name, age, and country). This app will also allow you to query those users through any of their attributes, and delete them.

Before you begin

This tutorial assumes you're already familiar with developing on Forge. If this is your first time using Forge, see Getting started for step-by-step instructions on setting up Forge.

To complete this tutorial, you need the following:

  • The latest version of Forge CLI. To update your CLI version, run npm install -g @forge/cli@latest on the command line.
  • An Atlassian site with Jira and Confluence Cloud where you can install your app.

Step 1: Create your app

Create an app based on the Hello world template. Using your terminal complete the following:

  1. Navigate to the directory where you want to create the app.

  2. Create your app by running:

    1
    2
    forge create
    
  3. Enter a name for the app. For example, user-management-via-entities.

  4. Select the UI kit category.

  5. Select the confluence-macro template.

  6. Your app has been created in a directory with the same name as your app; for example user-management-via-entities. Open the app directory to see the files associated with your app.

  7. Install the latest version of the Forge API package:

    1
    2
    npm install --save @forge/api@latest
    
  8. Install the UUID package, which will generate UUID keys required by our app:

    1
    2
    npm install --save uuid
    

Step 2: Configure the app manifest

We’ll add the scope required by the storage API and declare the entity we’ll use (along with its indexes).

  1. Enable the storage API by adding the storage:app scope to the manifest.yml file. Learn more about adding scopes to call an Atlassian REST API.
    1
    2
    permissions:
    scopes:
        - storage:app
    
  2. In the app section of the manifest.yml file, add the following entity declaration:
    1
    2
    app:
    ...
    storage:
        entities:
        - name: 'users'
            attributes:
            name:
                type: 'string'
            age:
                type: 'integer'
            country:
                type: 'string'
            indexes:
            - name: 'by-country'   
                range:
                - 'country'
            - name: 'by-country-name'   
                range:
                - 'name'
                partition:
                - 'country'
    

Here, we declare a users entity with the following attributes: name, age, and country. This declaration also consists of two indexes:

  • by-country: for querying by country
  • by-country-name: for querying by name and country, together

See Indexes for more details about how to construct an index.

Step 3: Deploy and install your app

  1. Navigate to the app's top-level directory and deploy your app by running:

    1
    2
    forge deploy
    
  2. Before installing your app, check that the indexes are already created first:

    1
    2
    forge storage entities indexes list -e development
    

    If the indexes were created successfully, this command should display the following:

    custom-entities-indexes-table-successful-command

  3. Install your app by running:

    1
    2
    forge install
    
  4. Select your Atlassian product using the arrow keys and press the enter key.

  5. Enter the URL for your development site. For example, example.atlassian.net. View a list of your active sites at Atlassian administration.

Once the successful installation message appears, your app is installed and ready to use on the specified site. You can always delete your app from the site by running the forge uninstall command.

Running the forge install command only installs your app onto the selected product. To install onto multiple products, repeat these steps again, selecting another product each time. Note that the Atlassian Marketplace does not support cross-product apps yet.

You must run forge deploy before running forge install in any of the Forge environments.

Step 4: Add an interface for creating users

We’ll now add a tab for creating users and assign a name, age, and country to them.

In the src/directory, add a new file named create-user.jsx with the following contents:

1
2
import ForgeUI, { TextField, Form, Select, Option, Fragment, SectionMessage, Text, useState } from '@forge/ui';
import { storage } from '@forge/api'
import { v4 as uuidv4 } from 'uuid';

export default function CreateUser() {
    const [userCreated, setUserCreated] = useState(false);

    const createUser = async (data) => {
        try {
            await storage
                .entity("users")
                .set(`user-${uuidv4()}`, {
                    ...data,
                    age: parseInt(data.age),
                });
            setUserCreated(true);
        } catch (e) {
            throw e;
        }
    }

    return (
        <Fragment>
            <Form onSubmit={createUser} submitButtonText="Create user">
                <TextField name="name" label="Name" type="text" />
                <TextField name="age" label="Age" type="number" />
                <Select name="country" label="Country">
                    <Option label="India" value="India" />
                    <Option label="Australia" value="Australia" />
                    <Option label="Indonesia" value="Indonesia" />
                </Select>
            </Form>
            {userCreated &&
                <SectionMessage title="Success" appearance="info">
                    <Text>User created successfully</Text>
                </SectionMessage>
            }
        </Fragment>
    )
}

This file will hold the form with the inputs as per your entity definition. This user form will let you create users. Later on, we’ll add the capabilities to query and delete users to this form.

The create-user.jsx file features the following:

  • A new component named CreateUser, which will contain the user form. This component uses a React state named userCreated, which we’ll use to display a message after a user is created.

  • A function named createUser which is a handler to the submit button in the form. This function makes a call to set storage operation and set userCreated state to true for showing the success message.

After creating the create-user.jsx file, continue with the next steps:

  1. Install the uuid package:

    1
    2
    npm install --save uuid
    

    The createUser function uses the uuid package to generate unique keys automatically each time a user is created. All attributes (name, age, and country) will be stored as values against each user’s unique key.

  2. Import and add a new Tab from Forge UI in index.jsx, which will hold this user form. Your index.jsx file should look like this:

    1
    2
    import ForgeUI,{ render, Fragment, Macro, Tabs, Tab } from "@forge/ui";
    import CreateUser from "./create-user";
    
    const App = () => {
    return (
        <Fragment>
        <Tabs>
            <Tab label="Create users">
            <CreateUser />
            </Tab>
        </Tabs>
        </Fragment>
    );
    };
    
    export const run = render(
    <Macro
        app={<App />}
    />
    );
    
  3. Re-deploy and verify your changes in the app by running:

    1
    2
    forge deploy
    
  4. Use the form to create several users, with multiple ones for each country from the selection. These will help us in the next step, where we will query these users based on their country and (or) their name.

View your app

With your app installed, it’s time to see the app on a page.

  1. Edit a Confluence page in your development site.
  2. Type /
  3. Find the macro app by name in the menu that appears and select it.
  4. Publish the page.

You should now be able to view the interface for creating users on the page: creating-user-indexes-interface

Step 5: Add a tab for querying users

After using the form to create users, let’s enhance it with the capability to query those user by country and age:

query-users-form-by-age-and-country

In the src/ directory, add a new file named user-view.jsx with the following contents:

1
2
import ForgeUI, { Table, Cell, Text, Head, Row } from '@forge/ui';

export default function UsersView({ users }) {
    return (
        <Table>
            <Head>
                <Cell>
                    <Text>Name</Text>
                </Cell>
                <Cell>
                    <Text>Country</Text>
                </Cell>
                <Cell>
                    <Text>Age</Text>
                </Cell>
            </Head>
            {
                users.map((user) => (
                    <Row>
                        <Cell>
                            <Text>{user.value.name}</Text>
                        </Cell>
                        <Cell>
                            <Text>{user.value.country}</Text>
                        </Cell>
                        <Cell>
                            <Text>{user.value.age}</Text>
                        </Cell>
                    </Row>
                ))
            }
        </Table>
    )
}

This file displays the queried data in a table. It also creates a component named UsersView which displays each user’s attributes. This component doesn’t have any state, and users are passed as a parameter.

Next, add a file in the src/ directory named query-user.jsx with the following contents:

1
2
import ForgeUI, { Fragment, TextField, Form, useState, Select, Option } from '@forge/ui';
import { storage, WhereConditions } from '@forge/api';
import UsersView from './user-view';

export default function QueryUsers({ }) {
    const [searchResultsByName, setSearchResultsByName] = useState([]);
    const searchByName = async (data) => {
        try {
            let queryBuilder = storage
                .entity("users")
                .query()
                .index('by-country-name', {
                    partition: [data.country]
                })
            if (data.name) {
                queryBuilder = queryBuilder
                    .where(WhereConditions.beginsWith(data.name));
            }
            const results = await queryBuilder
                .getMany();
            setSearchResultsByName(results.results);
        } catch (e) {
            throw e;
        }
    }
    return (
        <Fragment>
            <Form onSubmit={searchByName} submitButtonText="Search">
                <Select isRequired name="country" label="Country">
                    <Option label="India" value="India" />
                    <Option label="Australia" value="Australia" />
                    <Option label="Indonesia" value="Indonesia" />
                </Select>
                <TextField name="name" label="Search by name beginning with" type="text" />
            </Form>
            <UsersView users={searchResultsByName} />
        </Fragment>
    )
}

The query-user.jsx file will hold the form to submit our query parameters. This file:

  • Uses the by-country-name index to limit queries around a selected country.
  • Uses the beginsWith condition to filter partial or exact matches to the user’s name. See Filtering methods and Conditions for more information about building queries.
  • Uses a QueryUsers component that captures query results in the setSearchResultsByName state variable, which is being passed to the UsersView component (from the user-view.jsx file).
  • Uses the UsersView component to display the query results.

After creating both user-view.jsx and query-user.jsx files, continue with the next steps:

  1. Import and add QueryUsers in your index.jsx in a Tab view.

    1
    2
    ...
    import QueryUsers from "./query-user"
    ...
    
        ...
        <Tab label="Query users">
            <QueryUsers />
            </Tab>
        ...
    );
    };
    
  2. Re-deploy the app by running:

    1
    2
    forge deploy
    
  3. Test the app’s Query users tab using several queries, based on the users you created in the previous section.

    test-app-query-in-query-users-tab

Step 6: Call the Storage API’s custom entities endpoints

Finally, let’s add the following buttons to our Query users tab:

  • Get details: uses the storage.entity("entity-name").get endpoint to fetch details about a selected user, identified by its key. Learn more about the endpoint here.
  • Delete user: uses the storage.entity("entity-name").delete endpoint to trigger the deletion of a specific user, identified by its key. Learn more about the endpoint here.

query-users-tab-with-two-endpoints-get-details-and-delete-user

To do this, update the user-view.jsx with the following contents:

1
2
import ForgeUI, { Table, Cell, Text, Head, Row, ButtonSet, Button, useState, Code } from '@forge/ui';
import { storage } from '@forge/api';
export default function UsersView({ users }) {
    const [userDetail, setUserDetail] = useState(null);
    const getUserDetail = async (user) => {
        const userDetailResponse = await storage.entity('users').get(user.key);
        setUserDetail({
            key: user.key,
            value: userDetailResponse
        });
    };
    const deleteUser = async (userKey) => {
        await storage.entity('users').delete(userKey);
        setUserDetail(null);
    };
    return (
        <Table>
            <Head>
                <Cell>
                    <Text>Name</Text>
                </Cell>
                <Cell>
                    <Text>Country</Text>
                </Cell>
                <Cell>
                    <Text>Age</Text>
                </Cell>
                <Cell>
                    <Text>Actions</Text>
                </Cell>
                <Cell>
                    <Text>User details</Text>
                </Cell>
            </Head>
            {
                users.map((user) => (
                    <Row>
                        <Cell>
                            <Text>{user.value.name}</Text>
                        </Cell>
                        <Cell>
                            <Text>{user.value.country}</Text>
                        </Cell>
                        <Cell>
                            <Text>{user.value.age}</Text>
                        </Cell>
                        <Cell>
                            <ButtonSet>
                                <Button
                                    text='Get details'
                                    onClick={async () => {
                                        await getUserDetail(user);
                                    }}
                                />
                                <Button
                                    text='Delete user'
                                    onClick={async () => {
                                        await deleteUser(user.key);
                                    }}
                                />
                            </ButtonSet>
                        </Cell>
                        <Cell>
                            {
                                (userDetail && userDetail.key === user.key)
                                    ? (
                                        <Code
                                            text={JSON.stringify(userDetail, null, 2)}
                                            language="json"
                                        />
                                    )
                                    : null
                            }
                        </Cell>
                    </Row>
                ))
            }
        </Table>
    )
}

After updating user-view.jsx, re-deploy and verify the app by running:

1
2
forge deploy

You can now test both buttons to fetch data about a user or delete them. If needed, create more users to test custom entities even further.

Next steps

Explore the app storage API in further detail over the following pages:

Rate this page: