feat(automate): ability to delete automations (#4228)
* feat(automate): delete automation be * feat(automate): delete automations fe * fix(automate): delete modal, update cache * chore(automate): minor formatting * fix(automate): delete blobs w automations * chore(automate): repair blob test * fix(automate): make sure to return * fix(automate): do soft delete * fix(automate): include deleted filter in project automation queries
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
<template>
|
||||
<LayoutDialog
|
||||
v-model:open="isOpen"
|
||||
title="Delete automation"
|
||||
max-width="sm"
|
||||
:buttons="dialogButtons"
|
||||
>
|
||||
<p class="text-body-xs text-foreground">
|
||||
Are you sure you want to delete
|
||||
<span class="font-semibold">{{ automation.name }}</span>
|
||||
from your project?
|
||||
</p>
|
||||
<p v-if="automationFunction" class="text-body-xs text-foreground">
|
||||
You will still be able to use
|
||||
<span class="font-semibold">{{ automationFunction.name }}</span>
|
||||
in other automations, but all previous runs of this automation will be lost.
|
||||
</p>
|
||||
<p class="text-body-xs text-foreground">
|
||||
Model data will not be changed or deleted. Some automation data may be retained
|
||||
for auditing or security purposes.
|
||||
</p>
|
||||
</LayoutDialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { LayoutDialogButton } from '@speckle/ui-components'
|
||||
import { graphql } from '~/lib/common/generated/gql'
|
||||
import type { ProjectPageAutomationDeleteDialog_AutomationFragment } from '~/lib/common/generated/gql/graphql'
|
||||
import { projectRoute } from '~/lib/common/helpers/route'
|
||||
import { useMixpanel } from '~/lib/core/composables/mp'
|
||||
import { useDeleteAutomation } from '~/lib/projects/composables/automationManagement'
|
||||
|
||||
graphql(`
|
||||
fragment ProjectPageAutomationDeleteDialog_Project on Project {
|
||||
id
|
||||
name
|
||||
workspaceId
|
||||
}
|
||||
`)
|
||||
|
||||
graphql(`
|
||||
fragment ProjectPageAutomationDeleteDialog_Automation on Automation {
|
||||
id
|
||||
name
|
||||
currentRevision {
|
||||
functions {
|
||||
release {
|
||||
function {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`)
|
||||
|
||||
const props = defineProps<{
|
||||
projectId: string
|
||||
automation: ProjectPageAutomationDeleteDialog_AutomationFragment
|
||||
}>()
|
||||
|
||||
const isOpen = defineModel<boolean>('open', { required: true })
|
||||
|
||||
const router = useRouter()
|
||||
const mixpanel = useMixpanel()
|
||||
const deleteAutomation = useDeleteAutomation()
|
||||
|
||||
const handleDelete = async () => {
|
||||
const result = await deleteAutomation(props.projectId, props.automation.id)
|
||||
|
||||
if (result) {
|
||||
router.push(projectRoute(props.projectId, 'automations'))
|
||||
mixpanel.track('Automate Automation Deleted', {
|
||||
automationId: props.automation.id
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const dialogButtons = computed((): LayoutDialogButton[] => [
|
||||
{
|
||||
text: 'Cancel',
|
||||
props: { color: 'outline' },
|
||||
onClick: () => {
|
||||
isOpen.value = false
|
||||
}
|
||||
},
|
||||
{
|
||||
text: 'Delete',
|
||||
props: {
|
||||
color: 'danger'
|
||||
},
|
||||
onClick: handleDelete
|
||||
}
|
||||
])
|
||||
|
||||
const automationFunction = computed(() => {
|
||||
return props.automation.currentRevision?.functions.at(0)?.release?.function
|
||||
})
|
||||
</script>
|
||||
@@ -2,13 +2,30 @@
|
||||
<div class="flex flex-col w-full">
|
||||
<div class="flex items-center justify-between h-6 mb-6">
|
||||
<h2 class="h6 font-medium">Runs</h2>
|
||||
<FormButton
|
||||
v-if="!automation.isTestAutomation && isEditable"
|
||||
:disabled="!automation.enabled"
|
||||
@click="onTrigger"
|
||||
>
|
||||
Trigger automation
|
||||
</FormButton>
|
||||
<div class="flex items-center gap-2">
|
||||
<LayoutMenu
|
||||
v-model:open="showActionsMenu"
|
||||
:items="actionItems"
|
||||
:menu-position="HorizontalDirection.Left"
|
||||
@click.stop.prevent
|
||||
@chosen="onActionChosen"
|
||||
>
|
||||
<FormButton
|
||||
color="subtle"
|
||||
hide-text
|
||||
:icon-right="EllipsisHorizontalIcon"
|
||||
class="!text-foreground-2"
|
||||
@click="showActionsMenu = true"
|
||||
></FormButton>
|
||||
</LayoutMenu>
|
||||
<FormButton
|
||||
v-if="!automation.isTestAutomation && isEditable"
|
||||
:disabled="!automation.enabled"
|
||||
@click="onTrigger"
|
||||
>
|
||||
Trigger automation
|
||||
</FormButton>
|
||||
</div>
|
||||
</div>
|
||||
<AutomateRunsTable
|
||||
:runs="automation.runs.items"
|
||||
@@ -16,6 +33,11 @@
|
||||
:automation-id="automation.id"
|
||||
/>
|
||||
<InfiniteLoading :settings="{ identifier }" @infinite="onInfiniteLoad" />
|
||||
<ProjectPageAutomationDeleteDialog
|
||||
v-model:open="showDeleteDialog"
|
||||
:project-id="projectId"
|
||||
:automation="automation"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
@@ -25,6 +47,8 @@ import { graphql } from '~/lib/common/generated/gql'
|
||||
import type { ProjectPageAutomationRuns_AutomationFragment } from '~/lib/common/generated/gql/graphql'
|
||||
import { useTriggerAutomation } from '~/lib/projects/composables/automationManagement'
|
||||
import { projectAutomationPagePaginatedRunsQuery } from '~/lib/projects/graphql/queries'
|
||||
import { EllipsisHorizontalIcon } from '@heroicons/vue/24/solid'
|
||||
import { HorizontalDirection, type LayoutMenuItem } from '@speckle/ui-components'
|
||||
|
||||
// TODO: Subscriptions for new runs
|
||||
|
||||
@@ -41,6 +65,7 @@ graphql(`
|
||||
totalCount
|
||||
cursor
|
||||
}
|
||||
...ProjectPageAutomationDeleteDialog_Automation
|
||||
}
|
||||
`)
|
||||
|
||||
@@ -50,6 +75,28 @@ const props = defineProps<{
|
||||
isEditable: boolean
|
||||
}>()
|
||||
|
||||
const showActionsMenu = ref(false)
|
||||
const showDeleteDialog = ref(false)
|
||||
|
||||
const actionItems = computed<LayoutMenuItem[][]>(() => [
|
||||
[
|
||||
{
|
||||
title: 'Delete automation',
|
||||
id: 'delete'
|
||||
}
|
||||
]
|
||||
])
|
||||
|
||||
const onActionChosen = async (params: { item: LayoutMenuItem }) => {
|
||||
const { item } = params
|
||||
|
||||
switch (item.id) {
|
||||
case 'delete': {
|
||||
showDeleteDialog.value = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const { identifier, onInfiniteLoad } = usePaginatedQuery({
|
||||
query: projectAutomationPagePaginatedRunsQuery,
|
||||
baseVariables: computed(() => ({
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
@@ -29,6 +29,7 @@ import {
|
||||
createAutomationMutation,
|
||||
createAutomationRevisionMutation,
|
||||
createTestAutomationMutation,
|
||||
deleteAutomationMutation,
|
||||
triggerAutomationMutation,
|
||||
updateAutomationMutation
|
||||
} from '~/lib/projects/graphql/mutations'
|
||||
@@ -64,6 +65,59 @@ export function useCreateAutomation() {
|
||||
}
|
||||
}
|
||||
|
||||
export function useDeleteAutomation() {
|
||||
const { activeUser } = useActiveUser()
|
||||
const { triggerNotification } = useGlobalToast()
|
||||
const { client: apollo } = useApolloClient()
|
||||
|
||||
return async (projectId: string, automationId: string) => {
|
||||
if (!activeUser.value) return
|
||||
|
||||
const result = await apollo
|
||||
.mutate({
|
||||
mutation: deleteAutomationMutation,
|
||||
variables: {
|
||||
projectId,
|
||||
automationId
|
||||
},
|
||||
update: (cache, res) => {
|
||||
const { data } = res
|
||||
if (!data?.projectMutations?.automationMutations?.delete) return
|
||||
modifyObjectField(
|
||||
cache,
|
||||
getCacheId('Project', projectId),
|
||||
'automations',
|
||||
({ value, helpers }) => {
|
||||
return {
|
||||
...value,
|
||||
items: value.items?.filter(
|
||||
(automation) => helpers.readField(automation, 'id') !== automationId
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
})
|
||||
.catch(convertThrowIntoFetchResult)
|
||||
|
||||
if (result?.data) {
|
||||
triggerNotification({
|
||||
type: ToastNotificationType.Success,
|
||||
title: 'Automation deleted'
|
||||
})
|
||||
} else {
|
||||
const errorMessage = getFirstErrorMessage(result?.errors)
|
||||
triggerNotification({
|
||||
type: ToastNotificationType.Danger,
|
||||
title: 'Failed to delete automation',
|
||||
description: errorMessage
|
||||
})
|
||||
}
|
||||
|
||||
return result?.data?.projectMutations?.automationMutations?.delete
|
||||
}
|
||||
}
|
||||
|
||||
export function useCreateTestAutomation() {
|
||||
const { activeUser } = useActiveUser()
|
||||
const { triggerNotification } = useGlobalToast()
|
||||
|
||||
@@ -221,6 +221,16 @@ export const createAutomationMutation = graphql(`
|
||||
}
|
||||
`)
|
||||
|
||||
export const deleteAutomationMutation = graphql(`
|
||||
mutation DeleteAutomation($projectId: ID!, $automationId: ID!) {
|
||||
projectMutations {
|
||||
automationMutations(projectId: $projectId) {
|
||||
delete(automationId: $automationId)
|
||||
}
|
||||
}
|
||||
}
|
||||
`)
|
||||
|
||||
export const updateAutomationMutation = graphql(`
|
||||
mutation UpdateAutomation($projectId: ID!, $input: ProjectAutomationUpdateInput!) {
|
||||
projectMutations {
|
||||
|
||||
@@ -314,6 +314,7 @@ extend type ServerInfo {
|
||||
type ProjectAutomationMutations {
|
||||
create(input: ProjectAutomationCreateInput!): Automation!
|
||||
update(input: ProjectAutomationUpdateInput!): Automation!
|
||||
delete(automationId: ID!): Boolean!
|
||||
createRevision(input: ProjectAutomationRevisionCreateInput!): AutomationRevision!
|
||||
"""
|
||||
Trigger an automation with a fake "version created" trigger. The "version created" will
|
||||
|
||||
@@ -52,6 +52,10 @@ export type UpdateAutomation = (
|
||||
automation: SetRequired<Partial<AutomationRecord>, 'id'>
|
||||
) => Promise<AutomationRecord>
|
||||
|
||||
export type MarkAutomationDeleted = (params: {
|
||||
automationId: string
|
||||
}) => Promise<boolean>
|
||||
|
||||
export type GetLatestVersionAutomationRuns = (
|
||||
params: {
|
||||
projectId: string
|
||||
@@ -208,3 +212,7 @@ export type TriggerAutomationRevisionRun = <
|
||||
export type GetProjectAutomationCount = (params: {
|
||||
projectId: string
|
||||
}) => Promise<number>
|
||||
|
||||
export type QueryAllAutomationFunctionRuns = (params: {
|
||||
automationId: string
|
||||
}) => AsyncGenerator<AutomationFunctionRunRecord[], void, unknown>
|
||||
|
||||
@@ -29,14 +29,16 @@ import {
|
||||
updateAutomationFactory,
|
||||
updateAutomationRunFactory,
|
||||
upsertAutomationFunctionRunFactory,
|
||||
upsertAutomationRunFactory
|
||||
upsertAutomationRunFactory,
|
||||
markAutomationDeletedFactory
|
||||
} from '@/modules/automate/repositories/automations'
|
||||
import {
|
||||
createAutomationFactory,
|
||||
createAutomationRevisionFactory,
|
||||
createTestAutomationFactory,
|
||||
getAutomationsStatusFactory,
|
||||
validateAndUpdateAutomationFactory
|
||||
validateAndUpdateAutomationFactory,
|
||||
deleteAutomationFactory
|
||||
} from '@/modules/automate/services/automationManagement'
|
||||
import {
|
||||
AuthCodePayloadAction,
|
||||
@@ -121,6 +123,7 @@ import {
|
||||
import { getEventBus } from '@/modules/shared/services/eventBus'
|
||||
import { getProjectDbClient } from '@/modules/multiregion/utils/dbSelector'
|
||||
import { BranchNotFoundError } from '@/modules/core/errors/branch'
|
||||
import { commandFactory } from '@/modules/shared/command'
|
||||
import { mapAuthToServerError } from '@/modules/shared/helpers/errorHelper'
|
||||
import { withOperationLogging } from '@/observability/domain/businessLogging'
|
||||
|
||||
@@ -706,6 +709,28 @@ export = (FF_AUTOMATE_MODULE_ENABLED
|
||||
}
|
||||
)
|
||||
},
|
||||
async delete(parent, input, context) {
|
||||
const projectDb = await getProjectDbClient({ projectId: parent.projectId })
|
||||
|
||||
await authorizeResolver(
|
||||
context.userId,
|
||||
parent.projectId,
|
||||
Roles.Stream.Owner,
|
||||
context.resourceAccessRules
|
||||
)
|
||||
|
||||
const deleteAutomation = commandFactory({
|
||||
db: projectDb,
|
||||
operationFactory: ({ db }) =>
|
||||
deleteAutomationFactory({
|
||||
deleteAutomation: markAutomationDeletedFactory({ db })
|
||||
})
|
||||
})
|
||||
|
||||
return await deleteAutomation({
|
||||
automationId: input.automationId
|
||||
})
|
||||
},
|
||||
async createRevision(parent, { input }, ctx) {
|
||||
const projectId = parent.projectId
|
||||
const automationId = input.automationId
|
||||
|
||||
@@ -8,6 +8,7 @@ export type AutomationRecord = {
|
||||
enabled: boolean
|
||||
createdAt: Date
|
||||
updatedAt: Date
|
||||
isDeleted: boolean
|
||||
} & (
|
||||
| {
|
||||
executionEngineAutomationId: string
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import { Knex } from 'knex'
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
await knex.schema.alterTable('automations', (table) => {
|
||||
table.boolean('isDeleted').notNullable().defaultTo(false)
|
||||
})
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
await knex.schema.alterTable('automations', (table) => {
|
||||
table.dropColumn('isDeleted')
|
||||
})
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import {
|
||||
MarkAutomationDeleted,
|
||||
GetActiveTriggerDefinitions,
|
||||
GetAutomation,
|
||||
GetAutomationProject,
|
||||
@@ -19,6 +20,7 @@ import {
|
||||
GetProjectAutomationCount,
|
||||
GetRevisionsFunctions,
|
||||
GetRevisionsTriggerDefinitions,
|
||||
QueryAllAutomationFunctionRuns,
|
||||
StoreAutomation,
|
||||
StoreAutomationRevision,
|
||||
StoreAutomationToken,
|
||||
@@ -69,7 +71,10 @@ import {
|
||||
} from '@/modules/core/graph/generated/graphql'
|
||||
import { StreamRecord } from '@/modules/core/helpers/types'
|
||||
|
||||
import { formatJsonArrayRecords } from '@/modules/shared/helpers/dbHelper'
|
||||
import {
|
||||
executeBatchedSelect,
|
||||
formatJsonArrayRecords
|
||||
} from '@/modules/shared/helpers/dbHelper'
|
||||
import {
|
||||
decodeCursor,
|
||||
decodeIsoDateCursor,
|
||||
@@ -310,6 +315,16 @@ export const storeAutomationFactory =
|
||||
return newAutomation
|
||||
}
|
||||
|
||||
export const markAutomationDeletedFactory =
|
||||
(deps: { db: Knex }): MarkAutomationDeleted =>
|
||||
async ({ automationId }) => {
|
||||
await tables.automations(deps.db).where({ id: automationId }).update({
|
||||
isDeleted: true
|
||||
})
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
export const storeAutomationTokenFactory =
|
||||
(deps: { db: Knex }): StoreAutomationToken =>
|
||||
async (automationToken: AutomationTokenRecord) => {
|
||||
@@ -427,6 +442,7 @@ export const getAutomationsFactory =
|
||||
.automations(deps.db)
|
||||
.select()
|
||||
.whereIn(Automations.col.id, automationIds)
|
||||
.andWhere(Automations.col.isDeleted, false)
|
||||
|
||||
if (projectId?.length) {
|
||||
q.andWhere(Automations.col.projectId, projectId)
|
||||
@@ -724,6 +740,27 @@ export const getAutomationRunsItemsFactory =
|
||||
}
|
||||
}
|
||||
|
||||
export const queryAllAutomationFunctionRunsFactory =
|
||||
(deps: { db: Knex }): QueryAllAutomationFunctionRuns =>
|
||||
({ automationId }) => {
|
||||
const automationFunctionRunsQuery = tables
|
||||
.automationRevisions(deps.db)
|
||||
.select<AutomationFunctionRunRecord[]>(...AutomationFunctionRuns.cols)
|
||||
.where({ automationId })
|
||||
.join<AutomationRunRecord>(
|
||||
AutomationRuns.name,
|
||||
AutomationRuns.col.automationRevisionId,
|
||||
AutomationRevisions.col.id
|
||||
)
|
||||
.join<AutomationFunctionRunRecord>(
|
||||
AutomationFunctionRuns.name,
|
||||
AutomationFunctionRuns.col.runId,
|
||||
AutomationRuns.col.id
|
||||
)
|
||||
|
||||
return executeBatchedSelect(automationFunctionRunsQuery)
|
||||
}
|
||||
|
||||
export type GetProjectAutomationsParams = {
|
||||
projectId: string
|
||||
args: ProjectAutomationsArgs
|
||||
@@ -733,7 +770,10 @@ const getProjectAutomationsBaseQueryFactory =
|
||||
(deps: { db: Knex }) => (params: GetProjectAutomationsParams) => {
|
||||
const { projectId, args } = params
|
||||
|
||||
const q = tables.automations(deps.db).where(Automations.col.projectId, projectId)
|
||||
const q = tables
|
||||
.automations(deps.db)
|
||||
.where(Automations.col.projectId, projectId)
|
||||
.andWhere({ isDeleted: false })
|
||||
|
||||
if (args.filter?.length) {
|
||||
q.andWhere(Automations.col.name, 'ilike', `%${args.filter}%`)
|
||||
|
||||
@@ -42,6 +42,7 @@ import { validateAutomationName } from '@/modules/automate/utils/automationConfi
|
||||
import {
|
||||
CreateAutomation,
|
||||
CreateStoredAuthCode,
|
||||
MarkAutomationDeleted,
|
||||
GetAutomation,
|
||||
GetEncryptionKeyPair,
|
||||
GetLatestVersionAutomationRuns,
|
||||
@@ -109,7 +110,8 @@ export const createAutomationFactory =
|
||||
enabled,
|
||||
projectId,
|
||||
executionEngineAutomationId,
|
||||
isTestAutomation: false
|
||||
isTestAutomation: false,
|
||||
isDeleted: false
|
||||
})
|
||||
|
||||
const automationTokenRecord = await storeAutomationToken({
|
||||
@@ -196,7 +198,8 @@ export const createTestAutomationFactory =
|
||||
enabled: true,
|
||||
projectId,
|
||||
executionEngineAutomationId: null,
|
||||
isTestAutomation: true
|
||||
isTestAutomation: true,
|
||||
isDeleted: false
|
||||
})
|
||||
|
||||
await eventEmit({
|
||||
@@ -240,6 +243,13 @@ export const createTestAutomationFactory =
|
||||
return automationRecord
|
||||
}
|
||||
|
||||
export const deleteAutomationFactory =
|
||||
(deps: { deleteAutomation: MarkAutomationDeleted }) =>
|
||||
async (params: { automationId: string }) => {
|
||||
const { automationId } = params
|
||||
return await deps.deleteAutomation({ automationId })
|
||||
}
|
||||
|
||||
export type ValidateAndUpdateAutomationDeps = {
|
||||
getAutomation: GetAutomation
|
||||
updateAutomation: UpdateAutomation
|
||||
|
||||
@@ -389,6 +389,7 @@ const createAppToken = createAppTokenFactory({
|
||||
projectId: project.id,
|
||||
executionEngineAutomationId: cryptoRandomString({ length: 10 }),
|
||||
isTestAutomation: false,
|
||||
isDeleted: false,
|
||||
userId
|
||||
}
|
||||
const automationToken = {
|
||||
@@ -490,6 +491,7 @@ const createAppToken = createAppTokenFactory({
|
||||
projectId: project.id,
|
||||
executionEngineAutomationId: cryptoRandomString({ length: 10 }),
|
||||
isTestAutomation: false,
|
||||
isDeleted: false,
|
||||
userId
|
||||
}
|
||||
const automationToken = {
|
||||
@@ -610,6 +612,7 @@ const createAppToken = createAppTokenFactory({
|
||||
executionEngineAutomationId: cryptoRandomString({ length: 10 }),
|
||||
userId: cryptoRandomString({ length: 10 }),
|
||||
isTestAutomation: false,
|
||||
isDeleted: false,
|
||||
revision: {
|
||||
id: cryptoRandomString({ length: 10 }),
|
||||
createdAt: new Date(),
|
||||
@@ -653,6 +656,7 @@ const createAppToken = createAppTokenFactory({
|
||||
executionEngineAutomationId: cryptoRandomString({ length: 10 }),
|
||||
userId: cryptoRandomString({ length: 10 }),
|
||||
isTestAutomation: false,
|
||||
isDeleted: false,
|
||||
revision: {
|
||||
publicKey,
|
||||
active: false,
|
||||
@@ -696,6 +700,7 @@ const createAppToken = createAppTokenFactory({
|
||||
enabled: true,
|
||||
executionEngineAutomationId: cryptoRandomString({ length: 10 }),
|
||||
isTestAutomation: false,
|
||||
isDeleted: false,
|
||||
revision: {
|
||||
publicKey,
|
||||
id: cryptoRandomString({ length: 10 }),
|
||||
@@ -746,6 +751,7 @@ const createAppToken = createAppTokenFactory({
|
||||
executionEngineAutomationId: cryptoRandomString({ length: 10 }),
|
||||
userId: cryptoRandomString({ length: 10 }),
|
||||
isTestAutomation: false,
|
||||
isDeleted: false,
|
||||
revision: {
|
||||
publicKey,
|
||||
id: cryptoRandomString({ length: 10 }),
|
||||
@@ -795,6 +801,7 @@ const createAppToken = createAppTokenFactory({
|
||||
executionEngineAutomationId: cryptoRandomString({ length: 10 }),
|
||||
userId: cryptoRandomString({ length: 10 }),
|
||||
isTestAutomation: false,
|
||||
isDeleted: false,
|
||||
revision: {
|
||||
id: cryptoRandomString({ length: 10 }),
|
||||
createdAt: new Date(),
|
||||
@@ -845,6 +852,7 @@ const createAppToken = createAppTokenFactory({
|
||||
executionEngineAutomationId: cryptoRandomString({ length: 10 }),
|
||||
userId: cryptoRandomString({ length: 10 }),
|
||||
isTestAutomation: false,
|
||||
isDeleted: false,
|
||||
revision: {
|
||||
id: cryptoRandomString({ length: 10 }),
|
||||
userId: cryptoRandomString({ length: 10 }),
|
||||
@@ -908,6 +916,7 @@ const createAppToken = createAppTokenFactory({
|
||||
executionEngineAutomationId: cryptoRandomString({ length: 10 }),
|
||||
userId: cryptoRandomString({ length: 10 }),
|
||||
isTestAutomation: false,
|
||||
isDeleted: false,
|
||||
revision: {
|
||||
id: cryptoRandomString({ length: 10 }),
|
||||
userId: cryptoRandomString({ length: 10 }),
|
||||
@@ -969,6 +978,7 @@ const createAppToken = createAppTokenFactory({
|
||||
executionEngineAutomationId: null,
|
||||
userId: cryptoRandomString({ length: 10 }),
|
||||
isTestAutomation: true,
|
||||
isDeleted: false,
|
||||
revision: {
|
||||
id: cryptoRandomString({ length: 10 }),
|
||||
userId: cryptoRandomString({ length: 10 }),
|
||||
|
||||
@@ -21,6 +21,11 @@ export type UpdateBlob = (params: {
|
||||
|
||||
export type DeleteBlob = (params: { id: string; streamId?: string }) => Promise<number>
|
||||
|
||||
export type FullyDeleteBlob = (params: {
|
||||
blobId: string
|
||||
streamId: string
|
||||
}) => Promise<void>
|
||||
|
||||
export type GetBlobMetadata = (params: {
|
||||
blobId: string
|
||||
streamId: string
|
||||
|
||||
@@ -346,17 +346,15 @@ export const init: SpeckleModule['init'] = async ({ app }) => {
|
||||
getProjectObjectStorage({ projectId: streamId })
|
||||
])
|
||||
|
||||
const getBlobMetadata = getBlobMetadataFactory({ db: projectDb })
|
||||
const deleteBlob = fullyDeleteBlobFactory({
|
||||
getBlobMetadata,
|
||||
deleteBlob: deleteBlobFactory({ db: projectDb })
|
||||
getBlobMetadata: getBlobMetadataFactory({ db: projectDb }),
|
||||
deleteBlob: deleteBlobFactory({ db: projectDb }),
|
||||
deleteObject: deleteObjectFactory({ storage: projectStorage })
|
||||
})
|
||||
const deleteObject = deleteObjectFactory({ storage: projectStorage })
|
||||
|
||||
await deleteBlob({
|
||||
streamId: req.params.streamId,
|
||||
blobId: req.params.blobId,
|
||||
deleteObject
|
||||
blobId: req.params.blobId
|
||||
})
|
||||
res.status(204).send()
|
||||
})
|
||||
|
||||
@@ -137,20 +137,16 @@ export const markUploadOverFileSizeLimitFactory =
|
||||
}
|
||||
|
||||
export const fullyDeleteBlobFactory =
|
||||
(deps: { getBlobMetadata: GetBlobMetadata; deleteBlob: DeleteBlob }) =>
|
||||
async ({
|
||||
streamId,
|
||||
blobId,
|
||||
deleteObject
|
||||
}: {
|
||||
streamId: string
|
||||
blobId: string
|
||||
deleteObject: (params: ObjectKeyPayload) => MaybeAsync<void>
|
||||
}) => {
|
||||
(deps: {
|
||||
getBlobMetadata: GetBlobMetadata
|
||||
deleteBlob: DeleteBlob
|
||||
deleteObject: DeleteObjectFromStorage
|
||||
}) =>
|
||||
async ({ streamId, blobId }: { streamId: string; blobId: string }) => {
|
||||
const { objectKey } = await deps.getBlobMetadata({
|
||||
streamId,
|
||||
blobId
|
||||
})
|
||||
await deleteObject({ objectKey: objectKey! })
|
||||
await deps.deleteObject({ objectKey: objectKey! })
|
||||
await deps.deleteBlob({ id: blobId, streamId })
|
||||
}
|
||||
|
||||
@@ -68,7 +68,8 @@ const markUploadOverFileSizeLimit = markUploadOverFileSizeLimitFactory({
|
||||
})
|
||||
const deleteBlob = fullyDeleteBlobFactory({
|
||||
getBlobMetadata,
|
||||
deleteBlob: deleteBlobFactory({ db })
|
||||
deleteBlob: deleteBlobFactory({ db }),
|
||||
deleteObject: async () => {}
|
||||
})
|
||||
|
||||
describe('Blob storage @blobstorage', () => {
|
||||
@@ -338,8 +339,7 @@ describe('Blob storage @blobstorage', () => {
|
||||
const blobId = blob.id
|
||||
const { objectKey } = await getBlobMetadata({ streamId, blobId })
|
||||
expect(objectKey).to.equal(blob.objectKey)
|
||||
const deleteObject = async () => {}
|
||||
await deleteBlob({ streamId, blobId, deleteObject })
|
||||
await deleteBlob({ streamId, blobId })
|
||||
try {
|
||||
await getBlobMetadata({ streamId, blobId })
|
||||
throw new Error('This should have thrown')
|
||||
|
||||
@@ -593,7 +593,8 @@ export const Automations = buildTableHelper('automations', [
|
||||
'updatedAt',
|
||||
'userId',
|
||||
'executionEngineAutomationId',
|
||||
'isTestAutomation'
|
||||
'isTestAutomation',
|
||||
'isDeleted'
|
||||
])
|
||||
|
||||
export const GendoAIRenders = buildTableHelper('gendo_ai_renders', [
|
||||
|
||||
@@ -2249,6 +2249,7 @@ export type ProjectAutomationMutations = {
|
||||
createRevision: AutomationRevision;
|
||||
createTestAutomation: Automation;
|
||||
createTestAutomationRun: TestAutomationRun;
|
||||
delete: Scalars['Boolean']['output'];
|
||||
/**
|
||||
* Trigger an automation with a fake "version created" trigger. The "version created" will
|
||||
* just refer to the last version of the model.
|
||||
@@ -2278,6 +2279,11 @@ export type ProjectAutomationMutationsCreateTestAutomationRunArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type ProjectAutomationMutationsDeleteArgs = {
|
||||
automationId: Scalars['ID']['input'];
|
||||
};
|
||||
|
||||
|
||||
export type ProjectAutomationMutationsTriggerArgs = {
|
||||
automationId: Scalars['ID']['input'];
|
||||
};
|
||||
@@ -6685,6 +6691,7 @@ export type ProjectAutomationMutationsResolvers<ContextType = GraphQLContext, Pa
|
||||
createRevision?: Resolver<ResolversTypes['AutomationRevision'], ParentType, ContextType, RequireFields<ProjectAutomationMutationsCreateRevisionArgs, 'input'>>;
|
||||
createTestAutomation?: Resolver<ResolversTypes['Automation'], ParentType, ContextType, RequireFields<ProjectAutomationMutationsCreateTestAutomationArgs, 'input'>>;
|
||||
createTestAutomationRun?: Resolver<ResolversTypes['TestAutomationRun'], ParentType, ContextType, RequireFields<ProjectAutomationMutationsCreateTestAutomationRunArgs, 'automationId'>>;
|
||||
delete?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType, RequireFields<ProjectAutomationMutationsDeleteArgs, 'automationId'>>;
|
||||
trigger?: Resolver<ResolversTypes['String'], ParentType, ContextType, RequireFields<ProjectAutomationMutationsTriggerArgs, 'automationId'>>;
|
||||
update?: Resolver<ResolversTypes['Automation'], ParentType, ContextType, RequireFields<ProjectAutomationMutationsUpdateArgs, 'input'>>;
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
|
||||
@@ -2229,6 +2229,7 @@ export type ProjectAutomationMutations = {
|
||||
createRevision: AutomationRevision;
|
||||
createTestAutomation: Automation;
|
||||
createTestAutomationRun: TestAutomationRun;
|
||||
delete: Scalars['Boolean']['output'];
|
||||
/**
|
||||
* Trigger an automation with a fake "version created" trigger. The "version created" will
|
||||
* just refer to the last version of the model.
|
||||
@@ -2258,6 +2259,11 @@ export type ProjectAutomationMutationsCreateTestAutomationRunArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type ProjectAutomationMutationsDeleteArgs = {
|
||||
automationId: Scalars['ID']['input'];
|
||||
};
|
||||
|
||||
|
||||
export type ProjectAutomationMutationsTriggerArgs = {
|
||||
automationId: Scalars['ID']['input'];
|
||||
};
|
||||
|
||||
@@ -57,8 +57,8 @@ You can get the best DX by typing your resolvers with the `Resolvers` type and t
|
||||
|
||||
### Migrations
|
||||
|
||||
To create new migrations use `yarn migrate create`. Note that migrations are only ever read from the `./dist` folder to avoid scenarious when both the TS and JS version of the same migration is executed, so if you ever create a new migration make sure
|
||||
you build the app into `/dist` if you want it to be applied.
|
||||
To create new migrations use `yarn migrate create`. Note that migrations are only ever read from the `./dist` folder to avoid scenarios when both the TS and JS version of the same migration is executed, so if you ever create a new migration make sure
|
||||
you build the app into `./dist` if you want it to be applied.
|
||||
|
||||
### CLI
|
||||
|
||||
|
||||
@@ -2230,6 +2230,7 @@ export type ProjectAutomationMutations = {
|
||||
createRevision: AutomationRevision;
|
||||
createTestAutomation: Automation;
|
||||
createTestAutomationRun: TestAutomationRun;
|
||||
delete: Scalars['Boolean']['output'];
|
||||
/**
|
||||
* Trigger an automation with a fake "version created" trigger. The "version created" will
|
||||
* just refer to the last version of the model.
|
||||
@@ -2259,6 +2260,11 @@ export type ProjectAutomationMutationsCreateTestAutomationRunArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type ProjectAutomationMutationsDeleteArgs = {
|
||||
automationId: Scalars['ID']['input'];
|
||||
};
|
||||
|
||||
|
||||
export type ProjectAutomationMutationsTriggerArgs = {
|
||||
automationId: Scalars['ID']['input'];
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user