Skip to content

Instantly share code, notes, and snippets.

@rudfoss
Created December 26, 2020 11:23
Show Gist options
  • Select an option

  • Save rudfoss/ec9ee155dcfd23b3e8157e2bb80e9d57 to your computer and use it in GitHub Desktop.

Select an option

Save rudfoss/ec9ee155dcfd23b3e8157e2bb80e9d57 to your computer and use it in GitHub Desktop.
This is a small class that wraps around an Azure Table service and makes it easier to work with and serialize/deserialize data from the table.
import azureStorage, { TableService } from "azure-storage"
import { promisify } from "util"
const entGen = azureStorage.TableUtilities.entityGenerator
export interface TableRow<TRowData> {
partitionKey: string
rowKey: string
timestamp: Date
data: TRowData
}
type TableRowValueTypes = boolean | number | Date | string
type TableRowEDMTypes = "Edm.DateTime" | "Edm.Guid" | "Edm.Int64" | "Edm.String" | "Edm.DateTime"
interface RawTableCell<TValue extends TableRowValueTypes, TType extends TableRowEDMTypes> {
_: TValue
$?: TType
}
interface RawTableRowBase {
".metadata": {
etag: string
}
PartitionKey: RawTableCell<string, "Edm.String">
RowKey: RawTableCell<string, "Edm.String">
Timestamp: RawTableCell<Date, "Edm.DateTime">
}
type RawTableRow = RawTableRowBase & Record<string, RawTableCell<any, any>>
/**
* A small, generic class for managing an Azure Table.
*/
export class AzureTable<TRowData> {
public constructor(public readonly tableService: TableService, public readonly tableName: string) {}
public async getRow(partitionKey: string, rowKey: string) {
try {
const entry = await promisify(this.tableService.retrieveEntity).call(
this.tableService,
this.tableName,
partitionKey,
rowKey
)
return entry ? AzureTable.deserializeData<TRowData>(entry as any) : undefined
} catch (error) {
if (error.statusCode === 404) {
return undefined
}
throw error
}
}
public async getRows(partitionKey?: string, limit = 1000) {
const query = new azureStorage.TableQuery().top(limit)
if (partitionKey) {
query.where("PartitionKey eq ?", partitionKey)
}
return this.getRowsByQuery(query)
}
public async getRowsByQuery(query: azureStorage.TableQuery): Promise<TableRow<TRowData>[]> {
try {
const rows: any = await promisify(this.tableService.queryEntities).call(
this.tableService,
this.tableName,
query,
null as any
)
return rows?.entries?.map(AzureTable.deserializeData) ?? []
} catch (error) {
if (error.statusCode === 404) {
return []
}
throw error
}
}
public async insertOrUpdateRow(partitionKey: string, rowKey: string, data: TRowData) {
return promisify(this.tableService.insertOrReplaceEntity).call(
this.tableService,
this.tableName,
AzureTable.serializeData(partitionKey, rowKey, data)
)
}
public async deleteRow(partitionKey: string, rowKey: string) {
return await promisify(this.tableService.deleteEntity).call(this.tableService, this.tableName, {
PartitionKey: entGen.String(partitionKey),
RowKey: entGen.String(rowKey)
})
}
public async ensureTableExists() {
return promisify(this.tableService.createTableIfNotExists).call(this.tableService, this.tableName)
}
public static async createAzureTable<TRowData>({
connectionString,
tableName
}: {
connectionString: string
tableName: string
}) {
if (!AzureTable.isTableNameSafe(tableName)) {
throw new Error("Table name must be 3-63 characters, alphanumeric and cannot start with a number.")
}
const tableService = azureStorage.createTableService(connectionString)
const azureTable = new AzureTable<TRowData>(tableService, tableName)
await azureTable.ensureTableExists()
return azureTable
}
public static isTableNameSafe(tableName: string) {
const safeTableNameRx = /^[a-z]{1}[a-z0-9]{2,62}$/i
return !!tableName.match(safeTableNameRx)
}
public static serializeData<TRowData>(partitionKey: string, rowKey: string, data: TRowData) {
const finalRow: Pick<RawTableRowBase, "PartitionKey" | "RowKey"> &
Record<string, Record<string, TableRowValueTypes>> = {
PartitionKey: entGen.String(partitionKey) as any,
RowKey: entGen.String(rowKey) as any
}
for (const [dataKey, dataValue] of Object.entries(data)) {
if (typeof dataValue === "boolean") {
finalRow[dataKey] = entGen.Boolean(dataValue) as any
continue
}
if (typeof dataValue === "number") {
finalRow[dataKey] = entGen.Double(dataValue) as any
continue
}
if (dataValue instanceof Date) {
finalRow[dataKey] = entGen.DateTime(dataValue) as any
continue
}
if (typeof dataValue === "object") {
finalRow[dataKey] = entGen.String(JSON.stringify(dataValue)) as any
continue
}
finalRow[dataKey] = entGen.String(dataValue) as any
}
return finalRow as any
}
public static deserializeData<TRowData>(rawRow: RawTableRow): TableRow<TRowData> {
const finalObject: TableRow<any> = {
partitionKey: rawRow.PartitionKey._,
rowKey: rawRow.RowKey._,
timestamp: rawRow.Timestamp._,
data: {}
}
for (const [key, value] of Object.entries(rawRow)) {
if (key === ".metadata" || key === "PartitionKey" || key === "RowKey" || key === "Timestamp") {
continue
}
finalObject.data[key] = value._
}
return finalObject as any
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment