Exposing Azure Table Storage Through Forest Admin
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.
- 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;