Skip to main content

Enrich with AppsDB

Now that your Showpad App is nearing completion, you can enrich it by using the AppsDB. This is essential for providing your app with offline capabilities.

To do so, you need to update your main.ts file. In essence, you need to add the code for:

  1. Store
  2. User-Scoped Entries
  3. Globally-Scoped Entries

The following sections provide a description of each operation that can be performed, as well as a link to the function to use.

tip

You can preview a completed end result, take a look at our working example.

Store

A store is the top-level object which groups multiple store entries together. In other words, it's where your Showpad data is stored. It can hold both user-scoped and globally-scoped entries (at the same time).

Your store must be registered before it can be used. This is done by using the createStore() function.

caution

If the store already exists, an error will be thrown.

User-Scoped Entries

User-scoped entries are intended for data that's unique to a particular user and only available to them. For example, if a user is having a conversation with a client and data needs to be stored, this data can only be retrieved by this particular user.

AppsDB allows you to perform the following CRUD operations for user-scoped entries.

Create or Update

You can create a new user-scoped entry or update an existing user-scoped entry with the setStoreEntryValue() function.

Read

To retrieve the value of a user-scoped entry, the getStoreEntryValue() function must be used.

To retrieve all user-scoped entries, the getStoreEntries() function must be used.

Delete

To permanently remove a single user-scoped entry, use the deleteStoreEntry() function.

Warning

This action is irreversible. Once an entry has been deleted, it can't be recovered.

Globally-Scoped Entries

Globally-scoped entries are any information that can be accessed and retrieved by all users. For example, marketing or sales materials for products.

AppsDB allows you to perform the following CRUD operations for globally-scoped entries.

Create or Update

You can create a new globally-scoped entry or update an existing globally-scoped entry with the setGlobalStoreEntryValue() function.

Read

To retrieve a globally-scoped entry, the getGlobalStoreEntryValue() function must be used.

To retrieve all globally-scoped entries, the getGlobalStoreEntries() function must be used.

Delete

To permanently remove a globally-scoped entry, use the deleteGlobalStoreEntry() function.

Warning

This action is irreversible. Once an entry has been deleted, it can't be recovered.

Working Example

If you want to jump right in, we've prepared an extensive example that you can copy and replace the contents of the main.ts file.

This example code will only work if executed by an administrator or owner user because following functions require create, update or delete permissions on globally scoped entries:

Administrator or owner users have those permissions. For non-administrator users, you need to use an OAuth client as explained in: Updates for non-admins.

This code will create a Showpad App with buttons to make the calls to AppsDB. The example includes inline comments to explain each step.

Working Example code
import { Showpad } from '@showpad/experience-app-sdk';
import './style.css';

type MyType = Showpad.Scalar & {
weight: number;
colors: string[];
};

const app = document.querySelector<HTMLDivElement>('#app')!;

const STORE_ID = 'create-experience-app-store';
const ENTRY_ID = 'create-experience-app-entry';

// The Experience App SDK will take care of encoding & decoding values.
// Objects, Arrays, ... are supported. Take a look at the Showpad.Scalar type.
const ENTRY_VALUE: MyType = {
weight: 555,
colors: ['red', 'orange'],
};

const ERROR_EMPTY_ENTRY_VALUE = 'Entry value is empty.';

const displaySuccessToast = (text: string): Promise<Showpad.ToastReason> =>
Showpad.displayToast({
type: 'success',
text,
actionText: 'dismiss',
});

const main = async (): Promise<void> => {
// Always wait for ShowpadLib to be loaded.
await Showpad.onShowpadLibLoaded();

try {
// This will only work as admin (or owner).
// An admin user also has the permissions to write to the global scope
// (and create stores).
const showpadInstance = await Showpad.getShowpadInstance('v3');

// 1. STORE

// 1.1 Create a new store. If the store already exists, an error will be thrown.
const buttonCreate = document.createElement('button');
buttonCreate.innerHTML = `Create Store`;

buttonCreate.addEventListener('click', async () => {
try {
await Showpad.createStore(STORE_ID, showpadInstance);
displaySuccessToast('Create Store');
} catch (error) {
// Show a native error toast, the error is not bubbled outside the
// event handler so we need to address it here.
Showpad.handleErrorWithToast(error);
}
});

app.append(buttonCreate);

// 2. USER ENTRIES - CRUD

// 2.1 Create or update a user entry.
const buttonPutUser = document.createElement('button');
buttonPutUser.innerHTML = `Put User Entry`;

buttonPutUser.addEventListener('click', async () => {
try {
await Showpad.setStoreEntryValue(STORE_ID, ENTRY_ID, ENTRY_VALUE);
displaySuccessToast('Put User Entry');
} catch (error) {
// Show a native error toast, the error is not bubbled outside the
// event handler so we need to address it here.
Showpad.handleErrorWithToast(error);
}
});

app.append(buttonPutUser);

// 2.2 Read a user entry.
const buttonGetUser = document.createElement('button');
buttonGetUser.innerHTML = `Get User Entry`;

buttonGetUser.addEventListener('click', async () => {
try {
const entryValue = await Showpad.getStoreEntryValue<MyType>(
STORE_ID,
ENTRY_ID
);
if (!entryValue) throw new Error(ERROR_EMPTY_ENTRY_VALUE);

displaySuccessToast(JSON.stringify(entryValue));
} catch (error) {
// Show a native error toast, the error is not bubbled outside the
// event handler so we need to address it here.
Showpad.handleErrorWithToast(error);
}
});

app.append(buttonGetUser);

// 2.3 Read user entries.
const buttonGetUsers = document.createElement('button');
buttonGetUsers.innerHTML = `Get User Entries`;

buttonGetUsers.addEventListener('click', async () => {
try {
const entries = await Showpad.getStoreEntries<MyType>(STORE_ID);
displaySuccessToast(JSON.stringify(entries));
} catch (error) {
// Show a native error toast, the error is not bubbled outside the
// event handler so we need to address it here.
Showpad.handleErrorWithToast(error);
}
});

app.append(buttonGetUsers);

// 2.4 Delete a user entry.
const buttonDeleteUser = document.createElement('button');
buttonDeleteUser.innerHTML = `Delete User Entry`;

buttonDeleteUser.addEventListener('click', async () => {
try {
await Showpad.deleteStoreEntry(STORE_ID, ENTRY_ID);
displaySuccessToast('Delete User Entry');
} catch (error) {
// Show a native error toast, the error is not bubbled outside the
// event handler so we need to address it here.
Showpad.handleErrorWithToast(error);
}
});

app.append(buttonDeleteUser);

// 3. GLOBAL ENTRIES - CRUD

// 3.1 Create or update a global entry.
const buttonPutGlobal = document.createElement('button');
buttonPutGlobal.innerHTML = `Put Global Entry`;

buttonPutGlobal.addEventListener('click', async () => {
try {
await Showpad.setGlobalStoreEntryValue(
STORE_ID,
ENTRY_ID,
ENTRY_VALUE,
showpadInstance
);
displaySuccessToast('Put Global Entry');
} catch (error) {
// Show a native error toast, the error is not bubbled outside the
// event handler so we need to address it here.
Showpad.handleErrorWithToast(error);
}
});

app.append(buttonPutGlobal);

// 3.2 Read a global entry.
const buttonGetGlobal = document.createElement('button');
buttonGetGlobal.innerHTML = `Get Global Entry`;

buttonGetGlobal.addEventListener('click', async () => {
try {
const entryValue = await Showpad.getGlobalStoreEntryValue<MyType>(
STORE_ID,
ENTRY_ID
);
if (!entryValue) throw new Error(ERROR_EMPTY_ENTRY_VALUE);

displaySuccessToast(JSON.stringify(entryValue));
} catch (error) {
// Show a native error toast, the error is not bubbled outside the
// event handler so we need to address it here.
Showpad.handleErrorWithToast(error);
}
});

app.append(buttonGetGlobal);

// 3.3 Read global entries.
const buttonGetGlobals = document.createElement('button');
buttonGetGlobals.innerHTML = `Get Global Entries`;

buttonGetGlobals.addEventListener('click', async () => {
try {
const entries = await Showpad.getGlobalStoreEntries<MyType>(STORE_ID);
displaySuccessToast(JSON.stringify(entries));
} catch (error) {
// Show a native error toast, the error is not bubbled outside the
// event handler so we need to address it here.
Showpad.handleErrorWithToast(error);
}
});

app.append(buttonGetGlobals);

// 3.4 Delete a global entry.
const buttonDeleteGlobal = document.createElement('button');
buttonDeleteGlobal.innerHTML = `Delete Global Entry`;

buttonDeleteGlobal.addEventListener('click', async () => {
try {
await Showpad.deleteGlobalStoreEntry(
STORE_ID,
ENTRY_ID,
showpadInstance
);
displaySuccessToast('Delete Global Entry');
} catch (error) {
// Show a native error toast, the error is not bubbled outside the
// event handler so we need to address it here.
Showpad.handleErrorWithToast(error);
}
});

app.append(buttonDeleteGlobal);
} catch (error) {
// Show a native error toast.
Showpad.handleErrorWithToast(error);
}
};

main();

Updates for Non-Admins

You can grant AppsDB global create, update, and delete permissions to non-administrator users. For this, you will need a configured OAuth client. The non-administrator users will then need to enter their password (once) to execute AppsDB functions that require those permissions.

Don't hard code secrets

You should never store the clientSecret hard coded in your Showpad App. It's better to store it in the configuration as explained in the following example.

Initially, you'll add labels and assets to your config.json file:

{
"version": 1,
"labels": {
"settings": {
"clientId": "CLIENT_ID",
"clientSecret": "CLIENT_SECRET",
"allowedEmails": "LIST OF ALLOWED EMAILS SEPARATED BY COMMA"
},
"views": {},
"components": {}
},
"contents": {}
}
Updates to Working Example code
// Add the Config interface holding the values we'll get from config.json
interface Config extends Showpad.ConfigJSON {
labels: {
settings: {
oauth: {
client_id: Showpad.Label;
client_secret: Showpad.Label;
allowed_emails: Showpad.Label;
};
};
};
contents: {};
}

// Add helper function to read the allowed_emails csv string
const ERROR_PARSE_LABEL = 'Could not parse label value as array.';
const csvToArray = (csv: Showpad.Label): string[] => {
try {
return csv.value.replace(/\s/g, '').split(',');
} catch (error) {
throw new Error(ERROR_PARSE_LABEL);
}
};

// Add a function to get a showpadInstance capable of writing to AppsDB
const getShowpadInstance = await () => {
const isAdmin = await Showpad.isAdmin()

if(isAmin) {
return Showpad.getShowpadInstance();
} else {
// Destructure labels retrieved from the Showpad.parseConfig() method.
// Pass the defined Config interface which extends Showpad.ConfigJSON as a generic.
const { labels } = await Showpad.parseConfig<Config>();

// Destructure the OAuth credentials retrieved from the labels
const { client_id, client_secret, allowed_emails } = labels.settings.oauth;

// Parse a label as an array if it's written with single quotes.
// Double quotes will mess up your config.json file.
const allowedEmails = csvToArray(allowed_emails);

// Return an interactive OAuth instance based on the configured params
// from the Experience App Editor.
// We need this instance to authorize a create, update, or delete of
// a GLOBAL scoped entry.
return Showpad.getShowpadOAuthInstanceInteractive(
client_id.value,
client_secret.value,
allowedEmails,
'v3'
);
}
}

// replace the first line of the try in main() by
// const showpadInstance = await Showpad.getShowpadInstance();
const showpadInstance = await getShowpadInstance();