Created
December 26, 2020 11:23
-
-
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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