Exposing Azure Table Storage Through Forest Admin

Andrew Varnon
3 min readDec 7, 2020

--

I needed to provide a user interface for administering Azure Table Storage instances. I decided to try this with Forest Admin Smart Collections since we were already using Forest Admin on my project.

  1. Install @azure/data-tables
npm install @azure/data-tables

Note: this package is currently in beta but it works well enough for my purposes

2. Add AZURE_STORAGE_CONNECTION_STRING to your environment variables

This is the Azure Storage connection string that you obtain from Azure Portal

3. Create a class to act as a wrapper around @azure/data-tables

const { TableClient } = require('@azure/data-tables');const azureTableStorageService = {
deleteEntityAsync: async (tableName, partitionKey, rowKey) => {
const client = TableClient.fromConnectionString(
process.env.AZURE_STORAGE_CONNECTION_STRING,
tableName);
await client.deleteEntity(partitionKey, rowKey);
},
getEntityAsync: async (tableName, partitionKey, rowKey) => {
const client = TableClient.fromConnectionString(
process.env.AZURE_STORAGE_CONNECTION_STRING,
tableName);
return client.getEntity(partitionKey, rowKey);
},
listEntitiesAsync: async (tableName) => {
const client = TableClient.fromConnectionString(
process.env.AZURE_STORAGE_CONNECTION_STRING,
tableName);
const azureResponse = client.listEntities();
const response = [];
for await (const entity of azureResponse) {
response.push(entity);
}
return response;
},
upsertEntityAsync: async (tableName, entity) => {
const client = TableClient.fromConnectionString(
process.env.AZURE_STORAGE_CONNECTION_STRING,
tableName);
await client.upsertEntity(entity, "Replace");
},
};
module.exports = azureTableStorageService;

4. Create a Forest entry for the Smart Collection

Note that I create a fake property called id which is a concatenation of the partition and row keys. This is to make the routes work later on.

const Liana = require('forest-express-sequelize');Liana.collection('myTable', {
fields: [{
field: 'etag',
type: 'String',
}, {
field: 'JsonData',
type: 'String',
}, {
field: 'partitionKey',
type: 'String',
}, {
field: 'rowKey',
type: 'String',
}, {
field: 'timestamp',
type: 'Date',
}, {
field: 'id',
type: 'String',
get: (formTemplate) => `${formTemplate.partitionKey};#${formTemplate.rowKey}`,
}],
});

5. Create the route for the Smart Collection

const express = require('express');
const { PermissionMiddlewareCreator } = require('forest-express-sequelize');
const azureTableStorageService = require("../services/azure-table-storage-service");
const { Deserializer } = require('jsonapi-serializer')
const { RecordSerializer } = require('forest-express-sequelize');
const router = express.Router();
const permissionMiddlewareCreator = new PermissionMiddlewareCreator('myTable');
const tableName = "myTables";
const myTablesDeserializer = new Deserializer('myTable', {
keyForAttribute: 'camelCase',
});
const recordSerializer = new RecordSerializer({ name: 'myTable' });// Create a My Table
router.post('/myTable', permissionMiddlewareCreator.create(), async (request, response, next) => {
try {
const record = await myTablesDeserializer.deserialize(request.body);
record.partitionKey = record['partition-key'];
record.rowKey = record['row-key'];
record.JsonData = record['json-data'];
delete record.id;
delete record['partition-key'];
delete record['row-key'];
delete record['json-data'];
await azureTableStorageService.upsertEntityAsync(tableName, record);
const recordSerialized = await recordSerializer.serialize(record);
await response.send(recordSerialized);
} catch (e) {
console.error(e);
next(e);
}
});
// Update a My Table
router.put('/myTable/:recordId', permissionMiddlewareCreator.update(), async (request, response, next) => {
try {
const record = await myTablesDeserializer.deserialize(request.body);
const parts = request.params.recordId.split(';#');
const myTableItem = await azureTableStorageService.getEntityAsync(tableName, parts[0], parts[1]);
myTableItem.JsonData = record['json-data'] || myTableItem.JsonData;await azureTableStorageService.upsertEntityAsync(tableName, myTableItem);const myTable = await recordSerializer.serialize(myTableItem);
response.send({ ...myTable });
} catch (e) {
console.error(e);
next(e);
}
});
// Delete a My Table
router.delete('/myTable/:recordId', permissionMiddlewareCreator.delete(), async (request, response, next) => {
try {
const parts = request.params.recordId.split(';#');
await azureTableStorageService.deleteEntityAsync(tableName, parts[0], parts[1]);
await response.status(204).send();} catch (e) {
console.error(e);
next(e);
}
});
// Get a list of keys from the table storage
router.get('/myTable', permissionMiddlewareCreator.list(), async (request, response, next) => {
try {
const myTableList = await azureTableStorageService.listEntitiesAsync(tableName);
const myTables = await recordSerializer.serialize(myTableList);response.send({ ...myTables, meta: { count: myTableList.length } });} catch (e) {
console.error(e);
next(e);
}
});
// Get a table storage entry
router.get('/myTable/:recordId', permissionMiddlewareCreator.details(), async (request, response, next) => {
try {
const parts = request.params.recordId.split(';#');
const myTableItem = await azureTableStorageService.getEntityAsync(tableName, parts[0], parts[1]);
const myTable = await recordSerializer.serialize(myTableItem);
response.send({ ...myTable });
} catch (e) {
console.error(e);
next(e);
}
});
// Delete a list of table storage entries
router.delete('/myTable', permissionMiddlewareCreator.delete(), async (request, response, next) => {
try {
for (const key of request.body.data.attributes.ids) {
const parts = key.split(';#');
await azureTableStorageService.deleteEntityAsync(tableName, parts[0], parts[1]);
}
} catch (e) {
console.error(e);
next(e);
}
});
module.exports = router;

--

--

Andrew Varnon
Andrew Varnon

Written by Andrew Varnon

I am a full stack developer and architect, specializing in .Net and Azure.