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:
Chuck Driesler
2025-04-22 20:22:44 +01:00
committed by GitHub
parent 68ae557767
commit 5c68f8a1da
23 changed files with 1651 additions and 1273 deletions
@@ -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
+4 -6
View File
@@ -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')
+2 -1
View File
@@ -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'];
};
+2 -2
View File
@@ -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'];
};