import { getEntityGenerator, Query, QueryTable, TranslateToObject, UpsertTable, CreateTableIfNotExists, DeleteEntity, GetBlobAsText, DeleteBlob, CreateContainerIfNotExists, CreateBlockBlobFromText, ListBlobsSegmentedWithPrefix } from '@feedyou/utils' import { Bot } from '../../../../schema' import semver from 'semver' interface TableVersion { PartitionKey: string RowKey: string Version: string PaddedVersion: string AuthorName: string AuthorId: string Name: string Title: string Timestamp: string } interface TableComment { PartitionKey: string RowKey: string StepId: string Text: string InReplyTo: string Resolved: null | { by: { id: string; name: string }; timestamp: number } Status: 'resolved' | 'unassigned' Author: { name: string; id: string } Timestamp: string ReversedTimestamp: string Assignee: { id: string } DialogId: string } export interface Comment { id: string author: { name: string id: string } text: string status: string stepId?: string inReplyTo?: string dialogId?: string assignee?: { id: string } resolved?: { by: { id: string name: string } timestamp: number } timestamp?: number } interface BotInfo { id: string name: string version: string rowKey: string } interface BotVersions { [x: string]: { id: string name: string rowKey: string versions: BotInfo[] } } export const COMMENTS_TABLE = 'Comments' export const TABLE = 'BotVersion' const CONFLICT_TABLE = 'BotVersionConflict' export const CONTAINER = 'bot-versions' const CONFLICT_CONTAINER = 'conflict-bot-versions' async function entityExists(table: string, partitionKey: string, rowKey: string) { const queryString = "PartitionKey eq '" + partitionKey + "' and RowKey eq '" + rowKey + "'" const result = await QueryTable(table, Query().where(queryString)) return result && result.entries && result.entries.length > 0 } export async function getBotVersions(id: string) { const bots = await TranslateToObject( await QueryTable(TABLE, Query().where('PartitionKey eq ?', id)) ) if (bots && bots.length > 0) { return bots.map(bot => { return { id: bot.PartitionKey, version: bot.Version, rowKey: bot.RowKey, author: { name: bot.AuthorName, id: bot.AuthorId }, title: bot.Title, timestamp: bot.Timestamp } }) } else { throw new Error('could not find bot ' + id) } } export async function saveBotToStorage(bot: Bot): Promise { const blobData = JSON.stringify(bot) const rowKey = getRowKeyString(bot.version) const paddedVersion = getPaddedVersionString(bot.version) const filename = getBlobFilename(bot.id, rowKey) const entityGenerator = getEntityGenerator() const entity = { PartitionKey: entityGenerator.String(bot.id), RowKey: entityGenerator.String(rowKey), Version: entityGenerator.String(bot.version), PaddedVersion: entityGenerator.String(paddedVersion), Name: entityGenerator.String(bot.name || bot.id), Title: entityGenerator.String(bot.versionTitle || ''), AuthorName: entityGenerator.String(bot.author && bot.author.name ? bot.author.name : ''), AuthorId: entityGenerator.String(bot.author && bot.author.id ? bot.author.id : '') } let container = CONTAINER let table = TABLE const exists = await entityExists(table, bot.id, rowKey) if (exists) { container = CONFLICT_CONTAINER table = CONFLICT_TABLE console.error('CONFLICT WITH ' + filename + '!!') } await CreateContainerIfNotExists(container) await CreateTableIfNotExists(table) await UpsertTable(table, entity, 'replace') await CreateBlockBlobFromText(container, filename, blobData) if (exists) { throw new Error( `Version ${bot.version} of bot ${bot.name || ''} ${ bot.id } already exists - created probably by someone else. Backup file of your version was just created, please contact administrator to resolve this conflict. Send him screenshot of this message.` ) } else { return undefined } } export async function getBotsFromStorage() { let allBots = await TranslateToObject(await QueryTable(TABLE)) const formattedBots: BotInfo[] = allBots.map(bot => { return { id: bot.PartitionKey, name: bot.Name || bot.PartitionKey, version: bot.Version, rowKey: bot.RowKey } }) const botVersions = formattedBots.reduce((versionsByBot, bot) => { // group all versions by bot id if (versionsByBot[bot.id]) { versionsByBot[bot.id].versions.push(bot) } else { versionsByBot[bot.id] = { id: bot.id, name: bot.name, rowKey: bot.rowKey, versions: [bot] } } return versionsByBot }, {}) const bots = await Object.keys(botVersions).reduce(async (accum, id) => { let envsByBot = await accum const bot = botVersions[id] let botData: string try { botData = await GetBlobAsText(CONTAINER, getBlobFilename(id, bot.rowKey)) } catch (err) { console.error( `ATTENTION! Bot with id ${botVersions[id].id} and name ${ botVersions[id].name } has no blob! Try to look for ${getBlobFilename(id, bot.rowKey)} in blob storage` ) return envsByBot } const tree = JSON.parse(botData) // sort versions and get envs from last bot version const treeEnvs = (tree && tree.environments && tree.environments.others) || [] const prodId = id.replace(/-test$/, '') const index = envsByBot ? envsByBot.findIndex(currentBot => currentBot.id.replace(/-test$/, '') === prodId) : -1 if (index >= 0) { //this should never get triggered, because botVersions are grouped by bot ids envsByBot[index].environments = [...envsByBot[index].environments, ...treeEnvs] } else { envsByBot.push({ id: prodId, name: bot.name, environments: treeEnvs }) } return envsByBot }, Promise.resolve<{ id?: string; name?: string; environments?: {}[] }[]>([])) return bots } function getBlobFilename(partition: string, version: string) { return partition + '-' + version + '.json' } function getRowKeyString(version: string | number) { const reversedVersionString = '' + (Number.MAX_SAFE_INTEGER - parseInt(getPaddedVersionString(version))) // Table Storage uses lexical order so don't forget to left-pad value with zeros return '0000000000000000'.substring(0, 16 - reversedVersionString.length) + reversedVersionString } function getPaddedVersionString(version: string | number) { if (typeof version === 'number') { version = version.toString() } return semver .coerce(version) .format() .split('.') .map(v => '000'.substring(0, 3 - v.length) + v) .join('') } export async function getBotById(id: string): Promise<{ bot: Bot }> { const botEntity = await TranslateToObject( await QueryTable( TABLE, Query() .top(1) .where('PartitionKey eq ?', id) ) ) if (botEntity && botEntity.length > 0) { const rowKey = botEntity[0].RowKey const filename = getBlobFilename(id, rowKey) const bot = await GetBlobAsText(CONTAINER, filename) return { bot: JSON.parse(bot) } } else { throw new Error(`could not find bot ${id}`) } } export async function getBotComments(id: string): Promise { const comments = await TranslateToObject( await QueryTable(COMMENTS_TABLE, Query().where('PartitionKey eq ?', id)) ) if (comments) { return comments .map(comment => { if (comment && Object.keys(comment).length > 0) { return { id: comment.RowKey, timestamp: comment.Timestamp && Date.parse(comment.Timestamp), stepId: comment.StepId, dialogId: comment.DialogId, inReplyTo: comment.InReplyTo, text: comment.Text, assignee: comment.Assignee, author: comment.Author, status: comment.Status, resolved: comment.Resolved } } else { return undefined } }) .filter(comment => !!comment) } else { return [] } } export async function saveBotComment(botId: string, comment: Comment) { if (comment) { const entGen = getEntityGenerator() const reversedTimestamp = 1e13 - comment.timestamp const entity = { PartitionKey: entGen.String(botId), RowKey: entGen.String(comment.id), StepId: entGen.String(comment.stepId), DialogId: entGen.String(comment.dialogId), InReplyTo: entGen.String(comment.inReplyTo), Text: entGen.String(comment.text), Timestamp: entGen.DateTime( comment.timestamp ? new Date(comment.timestamp) : Date.now().toString() ), ReversedTimestamp: entGen.Int32(reversedTimestamp), Assignee: entGen.String(comment.assignee && JSON.stringify(comment.assignee)), Author: entGen.String(comment.author && JSON.stringify(comment.author)), Status: entGen.String(comment.status), Resolved: entGen.String(comment.resolved && JSON.stringify(comment.resolved)) } await UpsertTable(COMMENTS_TABLE, entity, 'replace') } else { throw new Error('comment body is empty') } } export async function deleteBotWithId(id: string): Promise { let botVersions: TableVersion[] = [] try { botVersions = await TranslateToObject( await QueryTable(TABLE, Query().where('PartitionKey eq ?', id)) ) } catch (error) { console.error(error) botVersions = [] } if (botVersions && botVersions.length > 0) { for (const bot of botVersions) { await DeleteEntity(TABLE, bot.PartitionKey.toString(), bot.RowKey.toString()) } const botBlobs = await ListBlobsSegmentedWithPrefix(CONTAINER, id) for (const bot of botBlobs.entries) { await DeleteBlob(CONTAINER, bot.name) } } else { console.log(`BOT WITH ID ${id} DOESNT EXIST ANYWAY`) } }