Skip to main content

AppsDB

In this tutorial, you will learn to work with the AppsDB inside an Experience App. Along the way you will use knowledge from the first two tutorials.

Requirements

  • Showpad domain with administrator rights
  • NodeJS

App setup

Configure config.json

First, we need to update labels and contents inside the config.json file.

{
"version": 1,
"labels": {
"settings": {
"oauth": {
"client_id": "",
"client_secret": "",
"allowed_emails": "[]"
}
}
},
"contents": {}
}

Finally, we need to deploy our app so we can fill in the config.json values in the Experience App editor. These values can be used inside our Experience app using the Showpad.parseConfig() method.

App deployment

Bundle Experience App

Bundle the app by following the steps outlined in this guide.

Deploy the app

Deploy the app by following the steps outlined in this Help Center article, but do not click on Publish Experience yet.

Alter client_id, client_secret, and allowed_emails as explained in the above article. Use the credentials from the OAuth client created in the first tutorial. Add your current user email address to the array in the allowed_emails field and make sure to use single quotes. Now publish the experience app.

App development

Run development server

npm run dev

The server is now running on the URL returned by the command. Open your browser and go to this local URL, and you'll see the HTML content of the index.html file.

Run the Experience App CLI proxy command

Locally, we don't have access to the Showpad functionality. So we need to add an intermediate server providing us this functionality. The Experience app CLI provides a command to spin up this proxy server. Before using this command, we have to:

  1. Download the proxy app.
  2. Upload the proxy app to Showpad platform.
tip

Check this Help Center article for a well-detailed explanation on how to upload custom Experience Apps to Showpad.

Now run the proxy app:

npx experience-app-cli proxy

Use the AppsDB inside your app

Replace the contents of main.ts with the following code. The comments in the code will explain line by line what it does.

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

interface Config extends Showpad.ConfigJSON {
labels: {
settings: {
oauth: {
client_id: Showpad.Label;
client_secret: Showpad.Label;
allowed_emails: Showpad.Label;
};
};
};
contents: {};
}

interface MyInterface extends 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: MyInterface = {
weight: 555
color: ['red', 'orange']
}

const ERROR_PARSE_LABEL = 'Could not parse label value as array.';
const ERROR_EMPTY_ENTRY_VALUE = 'Entry value is empty.';

const labelToArray = (label: Showpad.Label): string[] => {
try {
const array = JSON.parse(label.value.replaceAll(`'`, `"`));
if (!Array.isArray(array)) throw new Error();

return array;
} catch (error) {
throw new Error(ERROR_PARSE_LABEL);
}
};

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 {
// 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 = labelToArray(allowed_emails);

// Create 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.
const showpadApi = await Showpad.getShowpadOAuthInstanceInteractive(
client_id.value,
client_secret.value,
allowedEmails,
'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, showpadApi);
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<MyInterface>(STORE_ID, ENTRY_ID);
if (!entryValue) throw new Error(ERROR_EMPTY_ENTRY_VALUE);

displaySuccessToast(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<MyInterface>(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, showpadApi);
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<MyInterface>(STORE_ID, ENTRY_ID);
if (!entryValue) throw new Error(ERROR_EMPTY_ENTRY_VALUE);

displaySuccessToast(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<MyInterface>(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, showpadApi);
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();

App deployment

Bundle Experience App

Bundle the app by following the steps outlined in this guide.

Deploy the app

Deploy the app by following the steps outlined in this Help Center article.