From 3b6bc0346f92a0133d371e76c521a46694cf79e3 Mon Sep 17 00:00:00 2001 From: Chuck Driesler Date: Wed, 15 May 2024 11:29:05 +0100 Subject: [PATCH 1/9] untested implementation --- .../assets/automate/typedefs/automate.graphql | 6 +- .../automate/graph/resolvers/automate.ts | 26 +- .../server/modules/automate/helpers/types.ts | 26 +- .../automate/repositories/automations.ts | 47 +- .../modules/automate/services/trigger.ts | 637 ++++++++++-------- .../modules/automate/tests/trigger.spec.ts | 10 +- .../modules/core/graph/generated/graphql.ts | 10 +- .../graph/generated/graphql.ts | 10 +- .../server/test/graphql/generated/graphql.ts | 10 +- 9 files changed, 446 insertions(+), 336 deletions(-) diff --git a/packages/server/assets/automate/typedefs/automate.graphql b/packages/server/assets/automate/typedefs/automate.graphql index 9b8ae47c9a..85c9d3dcd1 100644 --- a/packages/server/assets/automate/typedefs/automate.graphql +++ b/packages/server/assets/automate/typedefs/automate.graphql @@ -1,8 +1,12 @@ enum AutomateRunStatus { + PENDING INITIALIZING RUNNING SUCCEEDED FAILED + EXCEPTION + TIMEOUT + CANCELED } enum AutomateRunTriggerType { @@ -324,7 +328,7 @@ extend type Query { extend type Mutation { automateFunctionRunStatusReport( - input: [AutomateFunctionRunStatusReportInput!]! + input: AutomateFunctionRunStatusReportInput! ): Boolean! @hasServerRole(role: SERVER_GUEST) @hasScope(scope: "automate:report-results") diff --git a/packages/server/modules/automate/graph/resolvers/automate.ts b/packages/server/modules/automate/graph/resolvers/automate.ts index b87ee828ff..b4db57af21 100644 --- a/packages/server/modules/automate/graph/resolvers/automate.ts +++ b/packages/server/modules/automate/graph/resolvers/automate.ts @@ -13,12 +13,14 @@ import { getAutomationRunsItems, getAutomationRunsTotalCount, getAutomationTriggerDefinitions, + getFunctionRun, getLatestVersionAutomationRuns, getProjectAutomationsItems, getProjectAutomationsTotalCount, storeAutomation, storeAutomationRevision, - updateAutomation as updateDbAutomation + updateAutomation as updateDbAutomation, + upsertAutomationFunctionRun } from '@/modules/automate/repositories/automations' import { createAutomation, @@ -50,8 +52,10 @@ import { getBranchesByIds } from '@/modules/core/repositories/branches' import { + setFunctionRunStatusReport, manuallyTriggerAutomation, - triggerAutomationRevisionRun + triggerAutomationRevisionRun, + SetFunctionRunStatusReportDeps } from '@/modules/automate/services/trigger' import { AutomateFunctionReleaseNotFoundError, @@ -344,10 +348,10 @@ export = { releases: args?.cursor || args?.filter?.search || args?.limit ? { - cursor: args.cursor || undefined, - search: args.filter?.search || undefined, - limit: args.limit || undefined - } + cursor: args.cursor || undefined, + search: args.filter?.search || undefined, + limit: args.limit || undefined + } : {} }) @@ -524,6 +528,16 @@ export = { } }, Mutation: { + async automateFunctionRunStatusReport(_parent, { input }) { + const deps: SetFunctionRunStatusReportDeps = { + getAutomationFunctionRunRecord: getFunctionRun, + upsertAutomationFunctionRunRecord: upsertAutomationFunctionRun + } + + const result = await setFunctionRunStatusReport(deps)(input) + + return result + }, automateMutations: () => ({}) }, Subscription: { diff --git a/packages/server/modules/automate/helpers/types.ts b/packages/server/modules/automate/helpers/types.ts index a660855b83..aea860f08f 100644 --- a/packages/server/modules/automate/helpers/types.ts +++ b/packages/server/modules/automate/helpers/types.ts @@ -20,19 +20,25 @@ export type AutomationRevisionRecord = { publicKey: string } -export type AutomationRunStatus = +export type AutomateRunStatus = | 'pending' + | 'initializing' | 'running' - | 'success' - | 'failure' - | 'error' + | 'succeeded' + | 'failed' + | 'exception' + | 'timeout' + | 'canceled' -export const AutomationRunStatuses: Record = { +export const AutomateRunStatuses: Record = { pending: 'pending', + initializing: 'initializing', running: 'running', - success: 'success', - failure: 'failure', - error: 'error' + succeeded: 'succeeded', + failed: 'failed', + exception: 'exception', + timeout: 'timeout', + canceled: 'canceled' } export type AutomationRunRecord = { @@ -40,7 +46,7 @@ export type AutomationRunRecord = { automationRevisionId: string createdAt: Date updatedAt: Date - status: AutomationRunStatus + status: AutomateRunStatus executionEngineRunId: string | null } @@ -121,7 +127,7 @@ export type AutomationFunctionRunRecord = { functionReleaseId: string functionId: string elapsed: number - status: AutomationRunStatus + status: AutomateRunStatus createdAt: Date updatedAt: Date contextView: string | null diff --git a/packages/server/modules/automate/repositories/automations.ts b/packages/server/modules/automate/repositories/automations.ts index b8bff4fd36..e310b6871f 100644 --- a/packages/server/modules/automate/repositories/automations.ts +++ b/packages/server/modules/automate/repositories/automations.ts @@ -12,7 +12,7 @@ import { AutomationFunctionRunRecord, AutomationRevisionWithTriggersFunctions, AutomationTriggerType, - AutomationRunStatus, + AutomateRunStatus, VersionCreationTriggerType, isVersionCreatedTrigger } from '@/modules/automate/helpers/types' @@ -93,6 +93,21 @@ export async function getFullAutomationRevisionMetadata( } } +export type InsertableAutomationFunctionRun = Omit + +export async function upsertAutomationFunctionRun(automationFunctionRun: InsertableAutomationFunctionRun) { + await AutomationFunctionRuns.knex() + .insert(_.pick(automationFunctionRun, AutomationFunctionRuns.withoutTablePrefix.cols)) + .onConflict(AutomationFunctionRuns.withoutTablePrefix.col.id) + .merge([ + AutomationFunctionRuns.withoutTablePrefix.col.contextView, + AutomationFunctionRuns.withoutTablePrefix.col.elapsed, + AutomationFunctionRuns.withoutTablePrefix.col.results, + AutomationFunctionRuns.withoutTablePrefix.col.status, + AutomationFunctionRuns.withoutTablePrefix.col.statusMessage + ]) +} + export type InsertableAutomationRun = AutomationRunRecord & { triggers: Omit[] functionRuns: Omit[] @@ -168,7 +183,7 @@ export async function getFunctionRun(functionRunId: string) { } export type GetFunctionRunsForAutomationRunIdsItem = AutomationFunctionRunRecord & { - automationRunStatus: AutomationRunStatus + AutomateRunStatus: AutomateRunStatus automationRunExecutionEngineId: string | null } @@ -186,7 +201,7 @@ export async function getFunctionRunsForAutomationRunIds(params: { const q = AutomationFunctionRuns.knex() .select>([ ...AutomationFunctionRuns.cols, - AutomationRuns.colAs('status', 'automationRunStatus'), + AutomationRuns.colAs('status', 'AutomateRunStatus'), AutomationRuns.colAs('executionEngineRunId', 'automationRunExecutionEngineId') ]) .innerJoin( @@ -248,11 +263,11 @@ export async function getFullAutomationRunById( return run ? { - ...formatJsonArrayRecords(run.runs)[0], - triggers: formatJsonArrayRecords(run.triggers), - functionRuns: formatJsonArrayRecords(run.functionRuns), - automationId: run.automationId - } + ...formatJsonArrayRecords(run.runs)[0], + triggers: formatJsonArrayRecords(run.triggers), + functionRuns: formatJsonArrayRecords(run.functionRuns), + automationId: run.automationId + } : null } @@ -336,11 +351,11 @@ export async function storeAutomationRevision(revision: InsertableAutomationRevi // Unset 'active in revision' for all other revisions ...(revision.active ? [ - AutomationRevisions.knex() - .where(AutomationRevisions.col.automationId, newRev.automationId) - .andWhereNot(AutomationRevisions.col.id, newRev.id) - .update(AutomationRevisions.withoutTablePrefix.col.active, false) - ] + AutomationRevisions.knex() + .where(AutomationRevisions.col.automationId, newRev.automationId) + .andWhereNot(AutomationRevisions.col.id, newRev.id) + .update(AutomationRevisions.withoutTablePrefix.col.active, false) + ] : []) ]) @@ -821,9 +836,9 @@ export const getAutomationProjects = async (params: { Automations.colAs('id', 'automationId'), ...(userId ? [ - // Getting first role from grouped results - knex.raw(`(array_agg("stream_acl"."role"))[1] as role`) - ] + // Getting first role from grouped results + knex.raw(`(array_agg("stream_acl"."role"))[1] as role`) + ] : []) ]) .whereIn(Automations.col.id, automationIds) diff --git a/packages/server/modules/automate/services/trigger.ts b/packages/server/modules/automate/services/trigger.ts index ba30e94a87..2638abac49 100644 --- a/packages/server/modules/automate/services/trigger.ts +++ b/packages/server/modules/automate/services/trigger.ts @@ -5,7 +5,9 @@ import { getFullAutomationRevisionMetadata, getAutomationToken, getAutomationTriggerDefinitions, - upsertAutomationRun + upsertAutomationRun, + upsertAutomationFunctionRun, + getFunctionRun } from '@/modules/automate/repositories/automations' import { AutomationWithRevision, @@ -14,7 +16,9 @@ import { VersionCreatedTriggerManifest, VersionCreationTriggerType, BaseTriggerManifest, - isVersionCreatedTriggerManifest + isVersionCreatedTriggerManifest, + AutomationFunctionRunRecord, + AutomateRunStatus } from '@/modules/automate/helpers/types' import { getBranchLatestCommits } from '@/modules/core/repositories/branches' import { getCommit } from '@/modules/core/repositories/commits' @@ -53,40 +57,40 @@ export type OnModelVersionCreateDeps = { */ export const onModelVersionCreate = (deps: OnModelVersionCreateDeps) => - async (params: { modelId: string; versionId: string; projectId: string }) => { - const { modelId, versionId, projectId } = params - const { getTriggers, triggerFunction } = deps - - // get triggers where modelId matches - const triggerDefinitions = await getTriggers({ - triggeringId: modelId, - triggerType: VersionCreationTriggerType - }) + async (params: { modelId: string; versionId: string; projectId: string }) => { + const { modelId, versionId, projectId } = params + const { getTriggers, triggerFunction } = deps - // get revisions where it matches any of the triggers and the revision is published - await Promise.all( - triggerDefinitions.map(async (tr) => { - try { - await triggerFunction({ - revisionId: tr.automationRevisionId, - manifest: { - versionId, - projectId, - modelId: tr.triggeringId, - triggerType: tr.triggerType - } - }) - } catch (error) { - // TODO: this error should be persisted for automation status display somehow - automateLogger.error( - 'Failure while triggering run onModelVersionCreate', - error, - params - ) - } + // get triggers where modelId matches + const triggerDefinitions = await getTriggers({ + triggeringId: modelId, + triggerType: VersionCreationTriggerType }) - ) - } + + // get revisions where it matches any of the triggers and the revision is published + await Promise.all( + triggerDefinitions.map(async (tr) => { + try { + await triggerFunction({ + revisionId: tr.automationRevisionId, + manifest: { + versionId, + projectId, + modelId: tr.triggeringId, + triggerType: tr.triggerType + } + }) + } catch (error) { + // TODO: this error should be persisted for automation status display somehow + automateLogger.error( + 'Failure while triggering run onModelVersionCreate', + error, + params + ) + } + }) + ) + } type InsertableAutomationRunWithExtendedFunctionRuns = Merge< InsertableAutomationRun, @@ -102,81 +106,136 @@ type CreateAutomationRunDataDeps = { const createAutomationRunData = (deps: CreateAutomationRunDataDeps) => - async (params: { - manifests: BaseTriggerManifest[] - automationWithRevision: AutomationWithRevision - }): Promise => { - const { getEncryptionKeyPairFor, getFunctionInputDecryptor } = deps - const { manifests, automationWithRevision } = params - const runId = cryptoRandomString({ length: 15 }) - const versionCreatedManifests = manifests.filter(isVersionCreatedTriggerManifest) - if (!versionCreatedManifests.length) { - throw new AutomateInvalidTriggerError( - 'Only version creation triggers currently supported' - ) - } - - const keyPair = await getEncryptionKeyPairFor( - automationWithRevision.revision.publicKey - ) - const functionInputDecryptor = await getFunctionInputDecryptor({ keyPair }) - let automationRun: InsertableAutomationRunWithExtendedFunctionRuns - try { - automationRun = { - id: runId, - triggers: [ - ...versionCreatedManifests.map((m) => ({ - triggeringId: m.versionId, - triggerType: m.triggerType - })) - ], - executionEngineRunId: null, - status: 'pending' as const, - automationRevisionId: automationWithRevision.revision.id, - createdAt: new Date(), - updatedAt: new Date(), - functionRuns: await Promise.all( - automationWithRevision.revision.functions.map(async (f) => ({ - functionId: f.functionId, - id: cryptoRandomString({ length: 15 }), - status: 'pending' as const, - elapsed: 0, - results: null, - contextView: null, - statusMessage: null, - resultVersions: [], - functionReleaseId: f.functionReleaseId, - functionInputs: await functionInputDecryptor.decryptInputs( - f.functionInputs - ), - createdAt: new Date(), - updatedAt: new Date() - })) - ) - } - } catch (e) { - if (e instanceof AutomationFunctionInputEncryptionError) { + async (params: { + manifests: BaseTriggerManifest[] + automationWithRevision: AutomationWithRevision + }): Promise => { + const { getEncryptionKeyPairFor, getFunctionInputDecryptor } = deps + const { manifests, automationWithRevision } = params + const runId = cryptoRandomString({ length: 15 }) + const versionCreatedManifests = manifests.filter(isVersionCreatedTriggerManifest) + if (!versionCreatedManifests.length) { throw new AutomateInvalidTriggerError( - 'One or more function inputs are not proper input objects', - { cause: e } + 'Only version creation triggers currently supported' ) } - if (e instanceof LibsodiumEncryptionError) { - throw new AutomateInvalidTriggerError( - 'Failed to decrypt one or more function inputs, they might not have been properly encrypted', - { cause: e } - ) + const keyPair = await getEncryptionKeyPairFor( + automationWithRevision.revision.publicKey + ) + const functionInputDecryptor = await getFunctionInputDecryptor({ keyPair }) + let automationRun: InsertableAutomationRunWithExtendedFunctionRuns + try { + automationRun = { + id: runId, + triggers: [ + ...versionCreatedManifests.map((m) => ({ + triggeringId: m.versionId, + triggerType: m.triggerType + })) + ], + executionEngineRunId: null, + status: 'pending' as const, + automationRevisionId: automationWithRevision.revision.id, + createdAt: new Date(), + updatedAt: new Date(), + functionRuns: await Promise.all( + automationWithRevision.revision.functions.map(async (f) => ({ + functionId: f.functionId, + id: cryptoRandomString({ length: 15 }), + status: 'pending' as const, + elapsed: 0, + results: null, + contextView: null, + statusMessage: null, + resultVersions: [], + functionReleaseId: f.functionReleaseId, + functionInputs: await functionInputDecryptor.decryptInputs( + f.functionInputs + ), + createdAt: new Date(), + updatedAt: new Date() + })) + ) + } + } catch (e) { + if (e instanceof AutomationFunctionInputEncryptionError) { + throw new AutomateInvalidTriggerError( + 'One or more function inputs are not proper input objects', + { cause: e } + ) + } + + if (e instanceof LibsodiumEncryptionError) { + throw new AutomateInvalidTriggerError( + 'Failed to decrypt one or more function inputs, they might not have been properly encrypted', + { cause: e } + ) + } + + throw e + } finally { + functionInputDecryptor.dispose() } - throw e - } finally { - functionInputDecryptor.dispose() + return automationRun } - return automationRun +const isValidNextStatus = (currentStatus: AutomateRunStatus, nextStatus: AutomateRunStatus): boolean => { + const statusRankMap: { [key in AutomateRunStatus]: number } = { + 'pending': 0, + 'initializing': 1, + 'running': 2, + 'succeeded': 3, + 'failed': 4, + 'exception': 5, + 'timeout': 6, + 'canceled': 7 } + return statusRankMap[nextStatus] > statusRankMap[currentStatus] +} + +const getElapsed = (from: Date): number => { + const now = new Date() + return now.getTime() - from.getTime() +} + +export type SetFunctionRunStatusReportDeps = { + getAutomationFunctionRunRecord: typeof getFunctionRun, + upsertAutomationFunctionRunRecord: typeof upsertAutomationFunctionRun +} + +export const setFunctionRunStatusReport = + (deps: SetFunctionRunStatusReportDeps) => + async (params: Pick): Promise => { + const { getAutomationFunctionRunRecord, upsertAutomationFunctionRunRecord } = deps + const { runId, ...statusReportData } = params + + const currentFunctionRunRecord = await getAutomationFunctionRunRecord(runId) + + if (!currentFunctionRunRecord) { + return false + } + + const currentStatus = currentFunctionRunRecord.status + const nextStatus = statusReportData.status + + if (!isValidNextStatus(currentStatus, nextStatus)) { + return false + } + + const nextFunctionRunRecord: AutomationFunctionRunRecord = { + ...currentFunctionRunRecord, + ...statusReportData, + elapsed: getElapsed(currentFunctionRunRecord.createdAt) + } + + await upsertAutomationFunctionRunRecord(nextFunctionRunRecord) + + return true + } + export type TriggerAutomationRevisionRunDeps = { automateRunTrigger: typeof triggerAutomationRun } & CreateAutomationRunDataDeps @@ -186,97 +245,97 @@ export type TriggerAutomationRevisionRunDeps = { */ export const triggerAutomationRevisionRun = (deps: TriggerAutomationRevisionRunDeps) => - async (params: { - revisionId: string - manifest: M - }): Promise<{ automationRunId: string }> => { - const { automateRunTrigger } = deps - const { revisionId, manifest } = params - - if (!isVersionCreatedTriggerManifest(manifest)) { - throw new AutomateInvalidTriggerError( - 'Only model version triggers are currently supported' - ) - } - - const { automationWithRevision, userId, automateToken } = await ensureRunConditions( - { - revisionGetter: getFullAutomationRevisionMetadata, - versionGetter: getCommit, - automationTokenGetter: getAutomationToken + async (params: { + revisionId: string + manifest: M + }): Promise<{ automationRunId: string }> => { + const { automateRunTrigger } = deps + const { revisionId, manifest } = params + + if (!isVersionCreatedTriggerManifest(manifest)) { + throw new AutomateInvalidTriggerError( + 'Only model version triggers are currently supported' + ) } - )({ - revisionId, - manifest - }) - const triggerManifests = await composeTriggerData({ - manifest, - projectId: automationWithRevision.projectId, - triggerDefinitions: automationWithRevision.revision.triggers - }) - - // TODO: Q Gergo: Should this really be project scoped? - const projectScopedToken = await createAppToken({ - appId: DefaultAppIds.Automate, - name: `at-${automationWithRevision.id}@${manifest.versionId}`, - userId, - // for now this is a baked in constant - // should rely on the function definitions requesting the needed scopes - scopes: [ - Scopes.Profile.Read, - Scopes.Streams.Read, - Scopes.Streams.Write, - Scopes.Automate.ReportResults - ], - limitResources: [ + const { automationWithRevision, userId, automateToken } = await ensureRunConditions( { - id: automationWithRevision.projectId, - type: TokenResourceIdentifierType.Project + revisionGetter: getFullAutomationRevisionMetadata, + versionGetter: getCommit, + automationTokenGetter: getAutomationToken } - ] - }) - - const automationRun = await createAutomationRunData(deps)({ - manifests: triggerManifests, - automationWithRevision - }) - await upsertAutomationRun(automationRun) + )({ + revisionId, + manifest + }) - try { - const { automationRunId } = await automateRunTrigger({ + const triggerManifests = await composeTriggerData({ + manifest, projectId: automationWithRevision.projectId, - automationId: automationWithRevision.executionEngineAutomationId, - manifests: triggerManifests, - functionRuns: automationRun.functionRuns.map((r) => ({ - ...r, - runId: automationRun.id - })), - speckleToken: projectScopedToken, - automationToken: automateToken + triggerDefinitions: automationWithRevision.revision.triggers }) - automationRun.executionEngineRunId = automationRunId - await upsertAutomationRun(automationRun) - } catch (error) { - const statusMessage = error instanceof Error ? error.message : `${error}` - automationRun.status = 'error' - automationRun.functionRuns = automationRun.functionRuns.map((fr) => ({ - ...fr, - status: 'error', - statusMessage - })) + // TODO: Q Gergo: Should this really be project scoped? + const projectScopedToken = await createAppToken({ + appId: DefaultAppIds.Automate, + name: `at-${automationWithRevision.id}@${manifest.versionId}`, + userId, + // for now this is a baked in constant + // should rely on the function definitions requesting the needed scopes + scopes: [ + Scopes.Profile.Read, + Scopes.Streams.Read, + Scopes.Streams.Write, + Scopes.Automate.ReportResults + ], + limitResources: [ + { + id: automationWithRevision.projectId, + type: TokenResourceIdentifierType.Project + } + ] + }) + + const automationRun = await createAutomationRunData(deps)({ + manifests: triggerManifests, + automationWithRevision + }) await upsertAutomationRun(automationRun) - } - await AutomateRunsEmitter.emit(AutomateRunsEmitter.events.Created, { - run: automationRun, - manifests: triggerManifests, - automation: automationWithRevision - }) + try { + const { automationRunId } = await automateRunTrigger({ + projectId: automationWithRevision.projectId, + automationId: automationWithRevision.executionEngineAutomationId, + manifests: triggerManifests, + functionRuns: automationRun.functionRuns.map((r) => ({ + ...r, + runId: automationRun.id + })), + speckleToken: projectScopedToken, + automationToken: automateToken + }) + + automationRun.executionEngineRunId = automationRunId + await upsertAutomationRun(automationRun) + } catch (error) { + const statusMessage = error instanceof Error ? error.message : `${error}` + automationRun.status = 'exception' + automationRun.functionRuns = automationRun.functionRuns.map((fr) => ({ + ...fr, + status: 'exception', + statusMessage + })) + await upsertAutomationRun(automationRun) + } - return { automationRunId: automationRun.id } - } + await AutomateRunsEmitter.emit(AutomateRunsEmitter.events.Created, { + run: automationRun, + manifests: triggerManifests, + automation: automationWithRevision + }) + + return { automationRunId: automationRun.id } + } export const ensureRunConditions = (deps: { @@ -284,71 +343,71 @@ export const ensureRunConditions = versionGetter: typeof getCommit automationTokenGetter: typeof getAutomationToken }) => - async (params: { - revisionId: string - manifest: M - }): Promise<{ - automationWithRevision: AutomationWithRevision - userId: string - automateToken: string - }> => { - const { revisionGetter, versionGetter, automationTokenGetter } = deps - const { revisionId, manifest } = params - const automationWithRevision = await revisionGetter(revisionId) - if (!automationWithRevision) - throw new AutomateInvalidTriggerError( - "Cannot trigger the given revision, it doesn't exist" - ) + async (params: { + revisionId: string + manifest: M + }): Promise<{ + automationWithRevision: AutomationWithRevision + userId: string + automateToken: string + }> => { + const { revisionGetter, versionGetter, automationTokenGetter } = deps + const { revisionId, manifest } = params + const automationWithRevision = await revisionGetter(revisionId) + if (!automationWithRevision) + throw new AutomateInvalidTriggerError( + "Cannot trigger the given revision, it doesn't exist" + ) - // if the automation is not active, do not trigger - if (!automationWithRevision.enabled) - throw new AutomateInvalidTriggerError( - 'The automation is not enabled, cannot trigger it' - ) + // if the automation is not active, do not trigger + if (!automationWithRevision.enabled) + throw new AutomateInvalidTriggerError( + 'The automation is not enabled, cannot trigger it' + ) - if (!automationWithRevision.revision.active) - throw new AutomateInvalidTriggerError( - 'The automation revision is not active, cannot trigger it' - ) + if (!automationWithRevision.revision.active) + throw new AutomateInvalidTriggerError( + 'The automation revision is not active, cannot trigger it' + ) - if (!isVersionCreatedTriggerManifest(manifest)) - throw new AutomateInvalidTriggerError('Only model version triggers are supported') + if (!isVersionCreatedTriggerManifest(manifest)) + throw new AutomateInvalidTriggerError('Only model version triggers are supported') - const triggerDefinition = automationWithRevision.revision.triggers.find((t) => { - if (t.triggerType !== manifest.triggerType) return false + const triggerDefinition = automationWithRevision.revision.triggers.find((t) => { + if (t.triggerType !== manifest.triggerType) return false - if (isVersionCreatedTriggerManifest(manifest)) { - return t.triggeringId === manifest.modelId - } + if (isVersionCreatedTriggerManifest(manifest)) { + return t.triggeringId === manifest.modelId + } - return false - }) + return false + }) - if (!triggerDefinition) - throw new AutomateInvalidTriggerError( - "The given revision doesn't have a trigger registered matching the input trigger" - ) + if (!triggerDefinition) + throw new AutomateInvalidTriggerError( + "The given revision doesn't have a trigger registered matching the input trigger" + ) - const triggeringVersion = await versionGetter(manifest.versionId) - if (!triggeringVersion) - throw new AutomateInvalidTriggerError('The triggering version is not found') + const triggeringVersion = await versionGetter(manifest.versionId) + if (!triggeringVersion) + throw new AutomateInvalidTriggerError('The triggering version is not found') - const userId = triggeringVersion.author - if (!userId) - throw new AutomateInvalidTriggerError( - "The user, that created the triggering version doesn't exist any more" - ) + const userId = triggeringVersion.author + if (!userId) + throw new AutomateInvalidTriggerError( + "The user, that created the triggering version doesn't exist any more" + ) - const token = await automationTokenGetter(automationWithRevision.id) - if (!token) - throw new AutomateInvalidTriggerError('Cannot find a token for the automation') + const token = await automationTokenGetter(automationWithRevision.id) + if (!token) + throw new AutomateInvalidTriggerError('Cannot find a token for the automation') - return { - automationWithRevision, - userId, - automateToken: token.automateToken + return { + automationWithRevision, + userId, + automateToken: token.automateToken + } } - } async function composeTriggerData(params: { projectId: string @@ -402,64 +461,64 @@ export type ManuallyTriggerAutomationDeps = { export const manuallyTriggerAutomation = (deps: ManuallyTriggerAutomationDeps) => - async (params: { - automationId: string - userId: string - projectId?: string - userResourceAccessRules?: ContextResourceAccessRules - }) => { - const { automationId, userId, projectId, userResourceAccessRules } = params - const { - getAutomationTriggerDefinitions, - getAutomation, - getBranchLatestCommits, - triggerFunction - } = deps - - const [automation, triggerDefs] = await Promise.all([ - getAutomation({ automationId, projectId }), - getAutomationTriggerDefinitions({ - automationId, - projectId, - triggerType: VersionCreationTriggerType - }) - ]) - if (!automation) { - throw new TriggerAutomationError('Automation not found') - } - if (!triggerDefs.length) { - throw new TriggerAutomationError( - 'No model version creation triggers found for the automation' - ) - } + async (params: { + automationId: string + userId: string + projectId?: string + userResourceAccessRules?: ContextResourceAccessRules + }) => { + const { automationId, userId, projectId, userResourceAccessRules } = params + const { + getAutomationTriggerDefinitions, + getAutomation, + getBranchLatestCommits, + triggerFunction + } = deps + + const [automation, triggerDefs] = await Promise.all([ + getAutomation({ automationId, projectId }), + getAutomationTriggerDefinitions({ + automationId, + projectId, + triggerType: VersionCreationTriggerType + }) + ]) + if (!automation) { + throw new TriggerAutomationError('Automation not found') + } + if (!triggerDefs.length) { + throw new TriggerAutomationError( + 'No model version creation triggers found for the automation' + ) + } - await validateStreamAccess( - userId, - automation.projectId, - Roles.Stream.Owner, - userResourceAccessRules - ) - - const validModelIds = triggerDefs.map((t) => t.triggeringId) - const [latestCommit] = await getBranchLatestCommits( - validModelIds, - automation.projectId, - { limit: 1 } - ) - if (!latestCommit) { - throw new TriggerAutomationError( - 'No version to trigger on found for the available triggers' + await validateStreamAccess( + userId, + automation.projectId, + Roles.Stream.Owner, + userResourceAccessRules ) - } - // Trigger "model version created" - return await triggerFunction({ - revisionId: triggerDefs[0].automationRevisionId, - manifest: { - projectId, - modelId: latestCommit.branchId, - versionId: latestCommit.id, - triggerType: VersionCreationTriggerType + const validModelIds = triggerDefs.map((t) => t.triggeringId) + const [latestCommit] = await getBranchLatestCommits( + validModelIds, + automation.projectId, + { limit: 1 } + ) + if (!latestCommit) { + throw new TriggerAutomationError( + 'No version to trigger on found for the available triggers' + ) } - }) - } + + // Trigger "model version created" + return await triggerFunction({ + revisionId: triggerDefs[0].automationRevisionId, + manifest: { + projectId, + modelId: latestCommit.branchId, + versionId: latestCommit.id, + triggerType: VersionCreationTriggerType + } + }) + } diff --git a/packages/server/modules/automate/tests/trigger.spec.ts b/packages/server/modules/automate/tests/trigger.spec.ts index 4a0fb1daa7..1552cff50c 100644 --- a/packages/server/modules/automate/tests/trigger.spec.ts +++ b/packages/server/modules/automate/tests/trigger.spec.ts @@ -6,7 +6,7 @@ import { triggerAutomationRevisionRun } from '@/modules/automate/services/trigger' import { - AutomationRunStatuses, + AutomateRunStatuses, AutomationTriggerDefinitionRecord, AutomationTriggerType, BaseTriggerManifest, @@ -1011,7 +1011,7 @@ const { FF_AUTOMATE_MODULE_ENABLED } = Environment.getFeatureFlags() automationRevisionId: createdRevision.id, createdAt: new Date(), updatedAt: new Date(), - status: AutomationRunStatuses.running, + status: AutomateRunStatuses.running, executionEngineRunId: cryptoRandomString({ length: 10 }), triggers: [ { @@ -1024,7 +1024,7 @@ const { FF_AUTOMATE_MODULE_ENABLED } = Environment.getFeatureFlags() functionId: generateFunctionId(), functionReleaseId: generateFunctionReleaseId(), id: cryptoRandomString({ length: 15 }), - status: AutomationRunStatuses.running, + status: AutomateRunStatuses.running, elapsed: 0, results: null, contextView: null, @@ -1180,8 +1180,8 @@ const { FF_AUTOMATE_MODULE_ENABLED } = Environment.getFeatureFlags() getFullAutomationRunById(automationRun.id), getFunctionRun(functionRunId) ]) - expect(updatedRun?.status).to.equal(AutomationRunStatuses.success) - expect(updatedFnRun?.status).to.equal(AutomationRunStatuses.success) + expect(updatedRun?.status).to.equal(AutomateRunStatuses.success) + expect(updatedFnRun?.status).to.equal(AutomateRunStatuses.success) expect(updatedFnRun?.contextView).to.equal(contextView) }) }) diff --git a/packages/server/modules/core/graph/generated/graphql.ts b/packages/server/modules/core/graph/generated/graphql.ts index 8de766d05f..a6407f5190 100644 --- a/packages/server/modules/core/graph/generated/graphql.ts +++ b/packages/server/modules/core/graph/generated/graphql.ts @@ -268,9 +268,9 @@ export type AutomateFunctionRun = { export type AutomateFunctionRunStatusReportInput = { contextView?: InputMaybe; - functionRunId: Scalars['String']; /** AutomateTypes.ResultsSchema type from @speckle/shared */ results?: InputMaybe; + runId: Scalars['String']; status: AutomateRunStatus; statusMessage?: InputMaybe; }; @@ -332,10 +332,14 @@ export type AutomateRunCollection = { }; export enum AutomateRunStatus { + Canceled = 'CANCELED', + Exception = 'EXCEPTION', Failed = 'FAILED', Initializing = 'INITIALIZING', + Pending = 'PENDING', Running = 'RUNNING', - Succeeded = 'SUCCEEDED' + Succeeded = 'SUCCEEDED', + Timeout = 'TIMEOUT' } export enum AutomateRunTriggerType { @@ -1321,7 +1325,7 @@ export type MutationAppUpdateArgs = { export type MutationAutomateFunctionRunStatusReportArgs = { - input: Array; + input: AutomateFunctionRunStatusReportInput; }; diff --git a/packages/server/modules/cross-server-sync/graph/generated/graphql.ts b/packages/server/modules/cross-server-sync/graph/generated/graphql.ts index 5bba482aa4..34a6dc334a 100644 --- a/packages/server/modules/cross-server-sync/graph/generated/graphql.ts +++ b/packages/server/modules/cross-server-sync/graph/generated/graphql.ts @@ -257,9 +257,9 @@ export type AutomateFunctionRun = { export type AutomateFunctionRunStatusReportInput = { contextView?: InputMaybe; - functionRunId: Scalars['String']; /** AutomateTypes.ResultsSchema type from @speckle/shared */ results?: InputMaybe; + runId: Scalars['String']; status: AutomateRunStatus; statusMessage?: InputMaybe; }; @@ -321,10 +321,14 @@ export type AutomateRunCollection = { }; export enum AutomateRunStatus { + Canceled = 'CANCELED', + Exception = 'EXCEPTION', Failed = 'FAILED', Initializing = 'INITIALIZING', + Pending = 'PENDING', Running = 'RUNNING', - Succeeded = 'SUCCEEDED' + Succeeded = 'SUCCEEDED', + Timeout = 'TIMEOUT' } export enum AutomateRunTriggerType { @@ -1310,7 +1314,7 @@ export type MutationAppUpdateArgs = { export type MutationAutomateFunctionRunStatusReportArgs = { - input: Array; + input: AutomateFunctionRunStatusReportInput; }; diff --git a/packages/server/test/graphql/generated/graphql.ts b/packages/server/test/graphql/generated/graphql.ts index c4c5360573..f1ed18a130 100644 --- a/packages/server/test/graphql/generated/graphql.ts +++ b/packages/server/test/graphql/generated/graphql.ts @@ -258,9 +258,9 @@ export type AutomateFunctionRun = { export type AutomateFunctionRunStatusReportInput = { contextView?: InputMaybe; - functionRunId: Scalars['String']; /** AutomateTypes.ResultsSchema type from @speckle/shared */ results?: InputMaybe; + runId: Scalars['String']; status: AutomateRunStatus; statusMessage?: InputMaybe; }; @@ -322,10 +322,14 @@ export type AutomateRunCollection = { }; export enum AutomateRunStatus { + Canceled = 'CANCELED', + Exception = 'EXCEPTION', Failed = 'FAILED', Initializing = 'INITIALIZING', + Pending = 'PENDING', Running = 'RUNNING', - Succeeded = 'SUCCEEDED' + Succeeded = 'SUCCEEDED', + Timeout = 'TIMEOUT' } export enum AutomateRunTriggerType { @@ -1311,7 +1315,7 @@ export type MutationAppUpdateArgs = { export type MutationAutomateFunctionRunStatusReportArgs = { - input: Array; + input: AutomateFunctionRunStatusReportInput; }; From 3d87f535028d751a2296224f2555ee2efe19dc3f Mon Sep 17 00:00:00 2001 From: Chuck Driesler Date: Wed, 15 May 2024 12:26:38 +0100 Subject: [PATCH 2/9] no more errors --- .../utils/automateFunctionRunStatus.ts | 80 +++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 packages/server/modules/automate/utils/automateFunctionRunStatus.ts diff --git a/packages/server/modules/automate/utils/automateFunctionRunStatus.ts b/packages/server/modules/automate/utils/automateFunctionRunStatus.ts new file mode 100644 index 0000000000..d426a0f412 --- /dev/null +++ b/packages/server/modules/automate/utils/automateFunctionRunStatus.ts @@ -0,0 +1,80 @@ +import { AutomationRunStatus, AutomationRunStatuses } from "@/modules/automate/helpers/types" +import { FunctionRunReportStatusesError } from '@/modules/automate/errors/runs' +import { AutomateRunStatus } from '@/modules/core/graph/generated/graphql' + +const AutomationRunStatusOrder: Array = [ + AutomationRunStatuses.pending, + AutomationRunStatuses.running, + [ + AutomationRunStatuses.exception, + AutomationRunStatuses.failed, + AutomationRunStatuses.succeeded + ] +] + +/** + * Given a previous and new status, verify that the new status is a valid move. + * @remarks This is to protect against race conditions that may report "backwards" motion + * in function statuses. (i.e. `FAILED` => `RUNNING`) + */ +export const validateStatusChange = ( + previousStatus: AutomationRunStatus, + newStatus: AutomationRunStatus +): void => { + if (previousStatus === newStatus) return + + const previousStatusIndex = AutomationRunStatusOrder.findIndex((s) => + Array.isArray(s) ? s.includes(previousStatus) : s === previousStatus + ) + const newStatusIndex = AutomationRunStatusOrder.findIndex((s) => + Array.isArray(s) ? s.includes(newStatus) : s === newStatus + ) + + if (newStatusIndex <= previousStatusIndex) { + throw new FunctionRunReportStatusesError( + `Invalid status change. Attempting to move from '${previousStatus}' to '${newStatus}'.` + ) + } +} + +export const mapGqlStatusToDbStatus = (status: AutomateRunStatus) => { + switch (status) { + case AutomateRunStatus.Pending: + return AutomationRunStatuses.pending + case AutomateRunStatus.Initializing: + return AutomationRunStatuses.initializing + case AutomateRunStatus.Running: + return AutomationRunStatuses.running + case AutomateRunStatus.Succeeded: + return AutomationRunStatuses.succeeded + case AutomateRunStatus.Failed: + return AutomationRunStatuses.failed + case AutomateRunStatus.Exception: + return AutomationRunStatuses.exception + case AutomateRunStatus.Timeout: + return AutomationRunStatuses.timeout + case AutomateRunStatus.Canceled: + return AutomationRunStatuses.canceled + } +} + +export const mapDbStatusToGqlStatus = (status: AutomationRunStatus) => { + switch (status) { + case AutomationRunStatuses.pending: + return AutomateRunStatus.Pending + case AutomationRunStatuses.initializing: + return AutomateRunStatus.Initializing + case AutomationRunStatuses.running: + return AutomateRunStatus.Running + case AutomationRunStatuses.succeeded: + return AutomateRunStatus.Succeeded + case AutomationRunStatuses.failed: + return AutomateRunStatus.Failed + case AutomationRunStatuses.exception: + return AutomateRunStatus.Exception + case AutomationRunStatuses.timeout: + return AutomateRunStatus.Timeout + case AutomationRunStatuses.canceled: + return AutomateRunStatus.Canceled + } +} \ No newline at end of file From cb9d0d646d971eb495958f683ea3b5b25d469978 Mon Sep 17 00:00:00 2001 From: Chuck Driesler Date: Wed, 15 May 2024 12:27:00 +0100 Subject: [PATCH 3/9] no more errors --- .../automate/graph/resolvers/automate.ts | 17 ++++- .../server/modules/automate/helpers/types.ts | 8 +- .../automate/repositories/automations.ts | 6 +- .../automate/services/runsManagement.ts | 74 +++---------------- .../modules/automate/services/trigger.ts | 32 ++------ .../modules/core/graph/generated/graphql.ts | 2 +- .../graph/generated/graphql.ts | 2 +- .../server/test/graphql/generated/graphql.ts | 2 +- 8 files changed, 40 insertions(+), 103 deletions(-) diff --git a/packages/server/modules/automate/graph/resolvers/automate.ts b/packages/server/modules/automate/graph/resolvers/automate.ts index b4db57af21..f94bb49dc3 100644 --- a/packages/server/modules/automate/graph/resolvers/automate.ts +++ b/packages/server/modules/automate/graph/resolvers/automate.ts @@ -66,7 +66,6 @@ import { dbToGraphqlTriggerTypeMap, functionTemplateRepos } from '@/modules/automate/helpers/executionEngine' -import { mapDbStatusToGqlStatus } from '@/modules/automate/services/runsManagement' import { authorizeResolver } from '@/modules/shared' import { AutomationRevisionFunctionForInputRedaction, @@ -83,6 +82,11 @@ import { ProjectSubscriptions, filteredSubscribe } from '@/modules/shared/utils/subscriptions' +import { + mapDbStatusToGqlStatus, + mapGqlStatusToDbStatus +} from '@/modules/automate/utils/automateFunctionRunStatus' + /** * TODO: @@ -534,7 +538,16 @@ export = { upsertAutomationFunctionRunRecord: upsertAutomationFunctionRun } - const result = await setFunctionRunStatusReport(deps)(input) + const payload = { + ...input, + contextView: input.contextView ?? null, + results: input.results as Automate.AutomateTypes.ResultsSchema ?? null, + runId: input.functionRunId, + status: mapGqlStatusToDbStatus(input.status), + statusMessage: input.statusMessage ?? null + } + + const result = await setFunctionRunStatusReport(deps)(payload) return result }, diff --git a/packages/server/modules/automate/helpers/types.ts b/packages/server/modules/automate/helpers/types.ts index aea860f08f..29482c1046 100644 --- a/packages/server/modules/automate/helpers/types.ts +++ b/packages/server/modules/automate/helpers/types.ts @@ -20,7 +20,7 @@ export type AutomationRevisionRecord = { publicKey: string } -export type AutomateRunStatus = +export type AutomationRunStatus = | 'pending' | 'initializing' | 'running' @@ -30,7 +30,7 @@ export type AutomateRunStatus = | 'timeout' | 'canceled' -export const AutomateRunStatuses: Record = { +export const AutomationRunStatuses: Record = { pending: 'pending', initializing: 'initializing', running: 'running', @@ -46,7 +46,7 @@ export type AutomationRunRecord = { automationRevisionId: string createdAt: Date updatedAt: Date - status: AutomateRunStatus + status: AutomationRunStatus executionEngineRunId: string | null } @@ -127,7 +127,7 @@ export type AutomationFunctionRunRecord = { functionReleaseId: string functionId: string elapsed: number - status: AutomateRunStatus + status: AutomationRunStatus createdAt: Date updatedAt: Date contextView: string | null diff --git a/packages/server/modules/automate/repositories/automations.ts b/packages/server/modules/automate/repositories/automations.ts index e310b6871f..8fe38d8549 100644 --- a/packages/server/modules/automate/repositories/automations.ts +++ b/packages/server/modules/automate/repositories/automations.ts @@ -12,7 +12,7 @@ import { AutomationFunctionRunRecord, AutomationRevisionWithTriggersFunctions, AutomationTriggerType, - AutomateRunStatus, + AutomationRunStatus, VersionCreationTriggerType, isVersionCreatedTrigger } from '@/modules/automate/helpers/types' @@ -183,7 +183,7 @@ export async function getFunctionRun(functionRunId: string) { } export type GetFunctionRunsForAutomationRunIdsItem = AutomationFunctionRunRecord & { - AutomateRunStatus: AutomateRunStatus + automationRunStatus: AutomationRunStatus automationRunExecutionEngineId: string | null } @@ -201,7 +201,7 @@ export async function getFunctionRunsForAutomationRunIds(params: { const q = AutomationFunctionRuns.knex() .select>([ ...AutomationFunctionRuns.cols, - AutomationRuns.colAs('status', 'AutomateRunStatus'), + AutomationRuns.colAs('status', 'automationRunStatus'), AutomationRuns.colAs('executionEngineRunId', 'automationRunExecutionEngineId') ]) .innerJoin( diff --git a/packages/server/modules/automate/services/runsManagement.ts b/packages/server/modules/automate/services/runsManagement.ts index 427576e73d..c864ef923f 100644 --- a/packages/server/modules/automate/services/runsManagement.ts +++ b/packages/server/modules/automate/services/runsManagement.ts @@ -13,69 +13,13 @@ import { updateFunctionRun } from '@/modules/automate/repositories/automations' import { - AutomateFunctionRunStatusReportInput, - AutomateRunStatus -} from '@/modules/core/graph/generated/graphql' + mapGqlStatusToDbStatus, + validateStatusChange +} from '@/modules/automate/utils/automateFunctionRunStatus' +import { AutomateFunctionRunStatusReportInput } from '@/modules/core/graph/generated/graphql' import { Automate } from '@speckle/shared' import { difference, groupBy, keyBy, mapValues, reduce, uniqBy } from 'lodash' -const AutomationRunStatusOrder: Array = [ - AutomationRunStatuses.pending, - AutomationRunStatuses.running, - [ - AutomationRunStatuses.error, - AutomationRunStatuses.failure, - AutomationRunStatuses.success - ] -] - -export const mapGqlStatusToDbStatus = (status: AutomateRunStatus) => { - switch (status) { - case AutomateRunStatus.Initializing: - return AutomationRunStatuses.pending - case AutomateRunStatus.Running: - return AutomationRunStatuses.running - case AutomateRunStatus.Succeeded: - return AutomationRunStatuses.success - case AutomateRunStatus.Failed: - return AutomationRunStatuses.failure - } -} - -export const mapDbStatusToGqlStatus = (status: AutomationRunStatus) => { - switch (status) { - case AutomationRunStatuses.pending: - return AutomateRunStatus.Initializing - case AutomationRunStatuses.running: - return AutomateRunStatus.Running - case AutomationRunStatuses.success: - return AutomateRunStatus.Succeeded - case AutomationRunStatuses.failure: - case AutomationRunStatuses.error: - return AutomateRunStatus.Failed - } -} - -const validateStatusChange = ( - previousStatus: AutomationRunStatus, - newStatus: AutomationRunStatus -) => { - if (previousStatus === newStatus) return - - const previousStatusIndex = AutomationRunStatusOrder.findIndex((s) => - Array.isArray(s) ? s.includes(previousStatus) : s === previousStatus - ) - const newStatusIndex = AutomationRunStatusOrder.findIndex((s) => - Array.isArray(s) ? s.includes(newStatus) : s === newStatus - ) - - if (newStatusIndex <= previousStatusIndex) { - throw new FunctionRunReportStatusesError( - `Invalid status change. Attempting to move from '${previousStatus}' to '${newStatus}'.` - ) - } -} - const validateContextView = (contextView: string) => { if (!contextView.length) { throw new FunctionRunReportStatusesError( @@ -112,13 +56,13 @@ export const resolveStatusFromFunctionRunStatuses = ( const anyRunning = functionRunStatuses.includes(AutomationRunStatuses.running) if (anyRunning) return AutomationRunStatuses.running - const anyError = functionRunStatuses.includes(AutomationRunStatuses.error) - if (anyError) return AutomationRunStatuses.error + const anyError = functionRunStatuses.includes(AutomationRunStatuses.exception) + if (anyError) return AutomationRunStatuses.exception - const anyFailure = functionRunStatuses.includes(AutomationRunStatuses.failure) - if (anyFailure) return AutomationRunStatuses.failure + const anyFailure = functionRunStatuses.includes(AutomationRunStatuses.failed) + if (anyFailure) return AutomationRunStatuses.failed - return AutomationRunStatuses.success + return AutomationRunStatuses.succeeded } export type ReportFunctionRunStatusesDeps = { diff --git a/packages/server/modules/automate/services/trigger.ts b/packages/server/modules/automate/services/trigger.ts index 2638abac49..7fd81d0c4e 100644 --- a/packages/server/modules/automate/services/trigger.ts +++ b/packages/server/modules/automate/services/trigger.ts @@ -17,8 +17,7 @@ import { VersionCreationTriggerType, BaseTriggerManifest, isVersionCreatedTriggerManifest, - AutomationFunctionRunRecord, - AutomateRunStatus + AutomationFunctionRunRecord } from '@/modules/automate/helpers/types' import { getBranchLatestCommits } from '@/modules/core/repositories/branches' import { getCommit } from '@/modules/core/repositories/commits' @@ -46,6 +45,7 @@ import { } from '@/modules/automate/services/encryption' import { LibsodiumEncryptionError } from '@/modules/shared/errors/encryption' import { AutomateRunsEmitter } from '@/modules/automate/events/runs' +import { validateStatusChange } from '@/modules/automate/utils/automateFunctionRunStatus' export type OnModelVersionCreateDeps = { getTriggers: typeof getActiveTriggerDefinitions @@ -181,26 +181,6 @@ const createAutomationRunData = return automationRun } -const isValidNextStatus = (currentStatus: AutomateRunStatus, nextStatus: AutomateRunStatus): boolean => { - const statusRankMap: { [key in AutomateRunStatus]: number } = { - 'pending': 0, - 'initializing': 1, - 'running': 2, - 'succeeded': 3, - 'failed': 4, - 'exception': 5, - 'timeout': 6, - 'canceled': 7 - } - - return statusRankMap[nextStatus] > statusRankMap[currentStatus] -} - -const getElapsed = (from: Date): number => { - const now = new Date() - return now.getTime() - from.getTime() -} - export type SetFunctionRunStatusReportDeps = { getAutomationFunctionRunRecord: typeof getFunctionRun, upsertAutomationFunctionRunRecord: typeof upsertAutomationFunctionRun @@ -221,14 +201,14 @@ export const setFunctionRunStatusReport = const currentStatus = currentFunctionRunRecord.status const nextStatus = statusReportData.status - if (!isValidNextStatus(currentStatus, nextStatus)) { - return false - } + validateStatusChange(currentStatus, nextStatus) + + const elapsed = new Date().getTime() - currentFunctionRunRecord.createdAt.getTime() const nextFunctionRunRecord: AutomationFunctionRunRecord = { ...currentFunctionRunRecord, ...statusReportData, - elapsed: getElapsed(currentFunctionRunRecord.createdAt) + elapsed } await upsertAutomationFunctionRunRecord(nextFunctionRunRecord) diff --git a/packages/server/modules/core/graph/generated/graphql.ts b/packages/server/modules/core/graph/generated/graphql.ts index a6407f5190..4f737bb481 100644 --- a/packages/server/modules/core/graph/generated/graphql.ts +++ b/packages/server/modules/core/graph/generated/graphql.ts @@ -268,9 +268,9 @@ export type AutomateFunctionRun = { export type AutomateFunctionRunStatusReportInput = { contextView?: InputMaybe; + functionRunId: Scalars['String']; /** AutomateTypes.ResultsSchema type from @speckle/shared */ results?: InputMaybe; - runId: Scalars['String']; status: AutomateRunStatus; statusMessage?: InputMaybe; }; diff --git a/packages/server/modules/cross-server-sync/graph/generated/graphql.ts b/packages/server/modules/cross-server-sync/graph/generated/graphql.ts index 34a6dc334a..46cdd0eec0 100644 --- a/packages/server/modules/cross-server-sync/graph/generated/graphql.ts +++ b/packages/server/modules/cross-server-sync/graph/generated/graphql.ts @@ -257,9 +257,9 @@ export type AutomateFunctionRun = { export type AutomateFunctionRunStatusReportInput = { contextView?: InputMaybe; + functionRunId: Scalars['String']; /** AutomateTypes.ResultsSchema type from @speckle/shared */ results?: InputMaybe; - runId: Scalars['String']; status: AutomateRunStatus; statusMessage?: InputMaybe; }; diff --git a/packages/server/test/graphql/generated/graphql.ts b/packages/server/test/graphql/generated/graphql.ts index f1ed18a130..262530ce48 100644 --- a/packages/server/test/graphql/generated/graphql.ts +++ b/packages/server/test/graphql/generated/graphql.ts @@ -258,9 +258,9 @@ export type AutomateFunctionRun = { export type AutomateFunctionRunStatusReportInput = { contextView?: InputMaybe; + functionRunId: Scalars['String']; /** AutomateTypes.ResultsSchema type from @speckle/shared */ results?: InputMaybe; - runId: Scalars['String']; status: AutomateRunStatus; statusMessage?: InputMaybe; }; From 8321f680c20230e1b97d455efa42eb781268822f Mon Sep 17 00:00:00 2001 From: Chuck Driesler Date: Wed, 15 May 2024 12:47:47 +0100 Subject: [PATCH 4/9] lint --- .../automate/graph/resolvers/automate.ts | 13 +- .../automate/repositories/automations.ts | 39 +- .../modules/automate/services/trigger.ts | 627 +++++++++--------- .../modules/automate/tests/trigger.spec.ts | 10 +- .../utils/automateFunctionRunStatus.ts | 7 +- 5 files changed, 355 insertions(+), 341 deletions(-) diff --git a/packages/server/modules/automate/graph/resolvers/automate.ts b/packages/server/modules/automate/graph/resolvers/automate.ts index f94bb49dc3..2da1f3e4ca 100644 --- a/packages/server/modules/automate/graph/resolvers/automate.ts +++ b/packages/server/modules/automate/graph/resolvers/automate.ts @@ -82,12 +82,11 @@ import { ProjectSubscriptions, filteredSubscribe } from '@/modules/shared/utils/subscriptions' -import { +import { mapDbStatusToGqlStatus, mapGqlStatusToDbStatus } from '@/modules/automate/utils/automateFunctionRunStatus' - /** * TODO: * - FE: @@ -352,10 +351,10 @@ export = { releases: args?.cursor || args?.filter?.search || args?.limit ? { - cursor: args.cursor || undefined, - search: args.filter?.search || undefined, - limit: args.limit || undefined - } + cursor: args.cursor || undefined, + search: args.filter?.search || undefined, + limit: args.limit || undefined + } : {} }) @@ -541,7 +540,7 @@ export = { const payload = { ...input, contextView: input.contextView ?? null, - results: input.results as Automate.AutomateTypes.ResultsSchema ?? null, + results: (input.results as Automate.AutomateTypes.ResultsSchema) ?? null, runId: input.functionRunId, status: mapGqlStatusToDbStatus(input.status), statusMessage: input.statusMessage ?? null diff --git a/packages/server/modules/automate/repositories/automations.ts b/packages/server/modules/automate/repositories/automations.ts index 8fe38d8549..07c046f52a 100644 --- a/packages/server/modules/automate/repositories/automations.ts +++ b/packages/server/modules/automate/repositories/automations.ts @@ -93,11 +93,18 @@ export async function getFullAutomationRevisionMetadata( } } -export type InsertableAutomationFunctionRun = Omit +export type InsertableAutomationFunctionRun = Omit< + AutomationFunctionRunRecord, + 'id' | 'runId' +> -export async function upsertAutomationFunctionRun(automationFunctionRun: InsertableAutomationFunctionRun) { +export async function upsertAutomationFunctionRun( + automationFunctionRun: InsertableAutomationFunctionRun +) { await AutomationFunctionRuns.knex() - .insert(_.pick(automationFunctionRun, AutomationFunctionRuns.withoutTablePrefix.cols)) + .insert( + _.pick(automationFunctionRun, AutomationFunctionRuns.withoutTablePrefix.cols) + ) .onConflict(AutomationFunctionRuns.withoutTablePrefix.col.id) .merge([ AutomationFunctionRuns.withoutTablePrefix.col.contextView, @@ -263,11 +270,11 @@ export async function getFullAutomationRunById( return run ? { - ...formatJsonArrayRecords(run.runs)[0], - triggers: formatJsonArrayRecords(run.triggers), - functionRuns: formatJsonArrayRecords(run.functionRuns), - automationId: run.automationId - } + ...formatJsonArrayRecords(run.runs)[0], + triggers: formatJsonArrayRecords(run.triggers), + functionRuns: formatJsonArrayRecords(run.functionRuns), + automationId: run.automationId + } : null } @@ -351,11 +358,11 @@ export async function storeAutomationRevision(revision: InsertableAutomationRevi // Unset 'active in revision' for all other revisions ...(revision.active ? [ - AutomationRevisions.knex() - .where(AutomationRevisions.col.automationId, newRev.automationId) - .andWhereNot(AutomationRevisions.col.id, newRev.id) - .update(AutomationRevisions.withoutTablePrefix.col.active, false) - ] + AutomationRevisions.knex() + .where(AutomationRevisions.col.automationId, newRev.automationId) + .andWhereNot(AutomationRevisions.col.id, newRev.id) + .update(AutomationRevisions.withoutTablePrefix.col.active, false) + ] : []) ]) @@ -836,9 +843,9 @@ export const getAutomationProjects = async (params: { Automations.colAs('id', 'automationId'), ...(userId ? [ - // Getting first role from grouped results - knex.raw(`(array_agg("stream_acl"."role"))[1] as role`) - ] + // Getting first role from grouped results + knex.raw(`(array_agg("stream_acl"."role"))[1] as role`) + ] : []) ]) .whereIn(Automations.col.id, automationIds) diff --git a/packages/server/modules/automate/services/trigger.ts b/packages/server/modules/automate/services/trigger.ts index 7fd81d0c4e..f6b7af0336 100644 --- a/packages/server/modules/automate/services/trigger.ts +++ b/packages/server/modules/automate/services/trigger.ts @@ -57,40 +57,40 @@ export type OnModelVersionCreateDeps = { */ export const onModelVersionCreate = (deps: OnModelVersionCreateDeps) => - async (params: { modelId: string; versionId: string; projectId: string }) => { - const { modelId, versionId, projectId } = params - const { getTriggers, triggerFunction } = deps + async (params: { modelId: string; versionId: string; projectId: string }) => { + const { modelId, versionId, projectId } = params + const { getTriggers, triggerFunction } = deps + + // get triggers where modelId matches + const triggerDefinitions = await getTriggers({ + triggeringId: modelId, + triggerType: VersionCreationTriggerType + }) - // get triggers where modelId matches - const triggerDefinitions = await getTriggers({ - triggeringId: modelId, - triggerType: VersionCreationTriggerType + // get revisions where it matches any of the triggers and the revision is published + await Promise.all( + triggerDefinitions.map(async (tr) => { + try { + await triggerFunction({ + revisionId: tr.automationRevisionId, + manifest: { + versionId, + projectId, + modelId: tr.triggeringId, + triggerType: tr.triggerType + } + }) + } catch (error) { + // TODO: this error should be persisted for automation status display somehow + automateLogger.error( + 'Failure while triggering run onModelVersionCreate', + error, + params + ) + } }) - - // get revisions where it matches any of the triggers and the revision is published - await Promise.all( - triggerDefinitions.map(async (tr) => { - try { - await triggerFunction({ - revisionId: tr.automationRevisionId, - manifest: { - versionId, - projectId, - modelId: tr.triggeringId, - triggerType: tr.triggerType - } - }) - } catch (error) { - // TODO: this error should be persisted for automation status display somehow - automateLogger.error( - 'Failure while triggering run onModelVersionCreate', - error, - params - ) - } - }) - ) - } + ) + } type InsertableAutomationRunWithExtendedFunctionRuns = Merge< InsertableAutomationRun, @@ -106,115 +106,120 @@ type CreateAutomationRunDataDeps = { const createAutomationRunData = (deps: CreateAutomationRunDataDeps) => - async (params: { - manifests: BaseTriggerManifest[] - automationWithRevision: AutomationWithRevision - }): Promise => { - const { getEncryptionKeyPairFor, getFunctionInputDecryptor } = deps - const { manifests, automationWithRevision } = params - const runId = cryptoRandomString({ length: 15 }) - const versionCreatedManifests = manifests.filter(isVersionCreatedTriggerManifest) - if (!versionCreatedManifests.length) { + async (params: { + manifests: BaseTriggerManifest[] + automationWithRevision: AutomationWithRevision + }): Promise => { + const { getEncryptionKeyPairFor, getFunctionInputDecryptor } = deps + const { manifests, automationWithRevision } = params + const runId = cryptoRandomString({ length: 15 }) + const versionCreatedManifests = manifests.filter(isVersionCreatedTriggerManifest) + if (!versionCreatedManifests.length) { + throw new AutomateInvalidTriggerError( + 'Only version creation triggers currently supported' + ) + } + + const keyPair = await getEncryptionKeyPairFor( + automationWithRevision.revision.publicKey + ) + const functionInputDecryptor = await getFunctionInputDecryptor({ keyPair }) + let automationRun: InsertableAutomationRunWithExtendedFunctionRuns + try { + automationRun = { + id: runId, + triggers: [ + ...versionCreatedManifests.map((m) => ({ + triggeringId: m.versionId, + triggerType: m.triggerType + })) + ], + executionEngineRunId: null, + status: 'pending' as const, + automationRevisionId: automationWithRevision.revision.id, + createdAt: new Date(), + updatedAt: new Date(), + functionRuns: await Promise.all( + automationWithRevision.revision.functions.map(async (f) => ({ + functionId: f.functionId, + id: cryptoRandomString({ length: 15 }), + status: 'pending' as const, + elapsed: 0, + results: null, + contextView: null, + statusMessage: null, + resultVersions: [], + functionReleaseId: f.functionReleaseId, + functionInputs: await functionInputDecryptor.decryptInputs( + f.functionInputs + ), + createdAt: new Date(), + updatedAt: new Date() + })) + ) + } + } catch (e) { + if (e instanceof AutomationFunctionInputEncryptionError) { throw new AutomateInvalidTriggerError( - 'Only version creation triggers currently supported' + 'One or more function inputs are not proper input objects', + { cause: e } ) } - const keyPair = await getEncryptionKeyPairFor( - automationWithRevision.revision.publicKey - ) - const functionInputDecryptor = await getFunctionInputDecryptor({ keyPair }) - let automationRun: InsertableAutomationRunWithExtendedFunctionRuns - try { - automationRun = { - id: runId, - triggers: [ - ...versionCreatedManifests.map((m) => ({ - triggeringId: m.versionId, - triggerType: m.triggerType - })) - ], - executionEngineRunId: null, - status: 'pending' as const, - automationRevisionId: automationWithRevision.revision.id, - createdAt: new Date(), - updatedAt: new Date(), - functionRuns: await Promise.all( - automationWithRevision.revision.functions.map(async (f) => ({ - functionId: f.functionId, - id: cryptoRandomString({ length: 15 }), - status: 'pending' as const, - elapsed: 0, - results: null, - contextView: null, - statusMessage: null, - resultVersions: [], - functionReleaseId: f.functionReleaseId, - functionInputs: await functionInputDecryptor.decryptInputs( - f.functionInputs - ), - createdAt: new Date(), - updatedAt: new Date() - })) - ) - } - } catch (e) { - if (e instanceof AutomationFunctionInputEncryptionError) { - throw new AutomateInvalidTriggerError( - 'One or more function inputs are not proper input objects', - { cause: e } - ) - } - - if (e instanceof LibsodiumEncryptionError) { - throw new AutomateInvalidTriggerError( - 'Failed to decrypt one or more function inputs, they might not have been properly encrypted', - { cause: e } - ) - } - - throw e - } finally { - functionInputDecryptor.dispose() + if (e instanceof LibsodiumEncryptionError) { + throw new AutomateInvalidTriggerError( + 'Failed to decrypt one or more function inputs, they might not have been properly encrypted', + { cause: e } + ) } - return automationRun + throw e + } finally { + functionInputDecryptor.dispose() } + return automationRun + } + export type SetFunctionRunStatusReportDeps = { - getAutomationFunctionRunRecord: typeof getFunctionRun, + getAutomationFunctionRunRecord: typeof getFunctionRun upsertAutomationFunctionRunRecord: typeof upsertAutomationFunctionRun } export const setFunctionRunStatusReport = (deps: SetFunctionRunStatusReportDeps) => - async (params: Pick): Promise => { - const { getAutomationFunctionRunRecord, upsertAutomationFunctionRunRecord } = deps - const { runId, ...statusReportData } = params - - const currentFunctionRunRecord = await getAutomationFunctionRunRecord(runId) - - if (!currentFunctionRunRecord) { - return false - } + async ( + params: Pick< + AutomationFunctionRunRecord, + 'runId' | 'status' | 'statusMessage' | 'contextView' | 'results' + > + ): Promise => { + const { getAutomationFunctionRunRecord, upsertAutomationFunctionRunRecord } = deps + const { runId, ...statusReportData } = params + + const currentFunctionRunRecord = await getAutomationFunctionRunRecord(runId) + + if (!currentFunctionRunRecord) { + return false + } - const currentStatus = currentFunctionRunRecord.status - const nextStatus = statusReportData.status + const currentStatus = currentFunctionRunRecord.status + const nextStatus = statusReportData.status - validateStatusChange(currentStatus, nextStatus) + validateStatusChange(currentStatus, nextStatus) - const elapsed = new Date().getTime() - currentFunctionRunRecord.createdAt.getTime() + const elapsed = new Date().getTime() - currentFunctionRunRecord.createdAt.getTime() - const nextFunctionRunRecord: AutomationFunctionRunRecord = { - ...currentFunctionRunRecord, - ...statusReportData, - elapsed - } + const nextFunctionRunRecord: AutomationFunctionRunRecord = { + ...currentFunctionRunRecord, + ...statusReportData, + elapsed + } - await upsertAutomationFunctionRunRecord(nextFunctionRunRecord) + await upsertAutomationFunctionRunRecord(nextFunctionRunRecord) - return true - } + return true + } export type TriggerAutomationRevisionRunDeps = { automateRunTrigger: typeof triggerAutomationRun @@ -225,97 +230,97 @@ export type TriggerAutomationRevisionRunDeps = { */ export const triggerAutomationRevisionRun = (deps: TriggerAutomationRevisionRunDeps) => - async (params: { - revisionId: string - manifest: M - }): Promise<{ automationRunId: string }> => { - const { automateRunTrigger } = deps - const { revisionId, manifest } = params - - if (!isVersionCreatedTriggerManifest(manifest)) { - throw new AutomateInvalidTriggerError( - 'Only model version triggers are currently supported' - ) + async (params: { + revisionId: string + manifest: M + }): Promise<{ automationRunId: string }> => { + const { automateRunTrigger } = deps + const { revisionId, manifest } = params + + if (!isVersionCreatedTriggerManifest(manifest)) { + throw new AutomateInvalidTriggerError( + 'Only model version triggers are currently supported' + ) + } + + const { automationWithRevision, userId, automateToken } = await ensureRunConditions( + { + revisionGetter: getFullAutomationRevisionMetadata, + versionGetter: getCommit, + automationTokenGetter: getAutomationToken } + )({ + revisionId, + manifest + }) + + const triggerManifests = await composeTriggerData({ + manifest, + projectId: automationWithRevision.projectId, + triggerDefinitions: automationWithRevision.revision.triggers + }) - const { automationWithRevision, userId, automateToken } = await ensureRunConditions( + // TODO: Q Gergo: Should this really be project scoped? + const projectScopedToken = await createAppToken({ + appId: DefaultAppIds.Automate, + name: `at-${automationWithRevision.id}@${manifest.versionId}`, + userId, + // for now this is a baked in constant + // should rely on the function definitions requesting the needed scopes + scopes: [ + Scopes.Profile.Read, + Scopes.Streams.Read, + Scopes.Streams.Write, + Scopes.Automate.ReportResults + ], + limitResources: [ { - revisionGetter: getFullAutomationRevisionMetadata, - versionGetter: getCommit, - automationTokenGetter: getAutomationToken + id: automationWithRevision.projectId, + type: TokenResourceIdentifierType.Project } - )({ - revisionId, - manifest - }) - - const triggerManifests = await composeTriggerData({ - manifest, - projectId: automationWithRevision.projectId, - triggerDefinitions: automationWithRevision.revision.triggers - }) + ] + }) - // TODO: Q Gergo: Should this really be project scoped? - const projectScopedToken = await createAppToken({ - appId: DefaultAppIds.Automate, - name: `at-${automationWithRevision.id}@${manifest.versionId}`, - userId, - // for now this is a baked in constant - // should rely on the function definitions requesting the needed scopes - scopes: [ - Scopes.Profile.Read, - Scopes.Streams.Read, - Scopes.Streams.Write, - Scopes.Automate.ReportResults - ], - limitResources: [ - { - id: automationWithRevision.projectId, - type: TokenResourceIdentifierType.Project - } - ] - }) + const automationRun = await createAutomationRunData(deps)({ + manifests: triggerManifests, + automationWithRevision + }) + await upsertAutomationRun(automationRun) - const automationRun = await createAutomationRunData(deps)({ + try { + const { automationRunId } = await automateRunTrigger({ + projectId: automationWithRevision.projectId, + automationId: automationWithRevision.executionEngineAutomationId, manifests: triggerManifests, - automationWithRevision + functionRuns: automationRun.functionRuns.map((r) => ({ + ...r, + runId: automationRun.id + })), + speckleToken: projectScopedToken, + automationToken: automateToken }) - await upsertAutomationRun(automationRun) - try { - const { automationRunId } = await automateRunTrigger({ - projectId: automationWithRevision.projectId, - automationId: automationWithRevision.executionEngineAutomationId, - manifests: triggerManifests, - functionRuns: automationRun.functionRuns.map((r) => ({ - ...r, - runId: automationRun.id - })), - speckleToken: projectScopedToken, - automationToken: automateToken - }) - - automationRun.executionEngineRunId = automationRunId - await upsertAutomationRun(automationRun) - } catch (error) { - const statusMessage = error instanceof Error ? error.message : `${error}` - automationRun.status = 'exception' - automationRun.functionRuns = automationRun.functionRuns.map((fr) => ({ - ...fr, - status: 'exception', - statusMessage - })) - await upsertAutomationRun(automationRun) - } + automationRun.executionEngineRunId = automationRunId + await upsertAutomationRun(automationRun) + } catch (error) { + const statusMessage = error instanceof Error ? error.message : `${error}` + automationRun.status = 'exception' + automationRun.functionRuns = automationRun.functionRuns.map((fr) => ({ + ...fr, + status: 'exception', + statusMessage + })) + await upsertAutomationRun(automationRun) + } - await AutomateRunsEmitter.emit(AutomateRunsEmitter.events.Created, { - run: automationRun, - manifests: triggerManifests, - automation: automationWithRevision - }) + await AutomateRunsEmitter.emit(AutomateRunsEmitter.events.Created, { + run: automationRun, + manifests: triggerManifests, + automation: automationWithRevision + }) - return { automationRunId: automationRun.id } - } + return { automationRunId: automationRun.id } + } export const ensureRunConditions = (deps: { @@ -323,71 +328,71 @@ export const ensureRunConditions = versionGetter: typeof getCommit automationTokenGetter: typeof getAutomationToken }) => - async (params: { - revisionId: string - manifest: M - }): Promise<{ - automationWithRevision: AutomationWithRevision - userId: string - automateToken: string - }> => { - const { revisionGetter, versionGetter, automationTokenGetter } = deps - const { revisionId, manifest } = params - const automationWithRevision = await revisionGetter(revisionId) - if (!automationWithRevision) - throw new AutomateInvalidTriggerError( - "Cannot trigger the given revision, it doesn't exist" - ) + async (params: { + revisionId: string + manifest: M + }): Promise<{ + automationWithRevision: AutomationWithRevision + userId: string + automateToken: string + }> => { + const { revisionGetter, versionGetter, automationTokenGetter } = deps + const { revisionId, manifest } = params + const automationWithRevision = await revisionGetter(revisionId) + if (!automationWithRevision) + throw new AutomateInvalidTriggerError( + "Cannot trigger the given revision, it doesn't exist" + ) - // if the automation is not active, do not trigger - if (!automationWithRevision.enabled) - throw new AutomateInvalidTriggerError( - 'The automation is not enabled, cannot trigger it' - ) + // if the automation is not active, do not trigger + if (!automationWithRevision.enabled) + throw new AutomateInvalidTriggerError( + 'The automation is not enabled, cannot trigger it' + ) - if (!automationWithRevision.revision.active) - throw new AutomateInvalidTriggerError( - 'The automation revision is not active, cannot trigger it' - ) + if (!automationWithRevision.revision.active) + throw new AutomateInvalidTriggerError( + 'The automation revision is not active, cannot trigger it' + ) - if (!isVersionCreatedTriggerManifest(manifest)) - throw new AutomateInvalidTriggerError('Only model version triggers are supported') + if (!isVersionCreatedTriggerManifest(manifest)) + throw new AutomateInvalidTriggerError('Only model version triggers are supported') - const triggerDefinition = automationWithRevision.revision.triggers.find((t) => { - if (t.triggerType !== manifest.triggerType) return false + const triggerDefinition = automationWithRevision.revision.triggers.find((t) => { + if (t.triggerType !== manifest.triggerType) return false - if (isVersionCreatedTriggerManifest(manifest)) { - return t.triggeringId === manifest.modelId - } + if (isVersionCreatedTriggerManifest(manifest)) { + return t.triggeringId === manifest.modelId + } - return false - }) + return false + }) - if (!triggerDefinition) - throw new AutomateInvalidTriggerError( - "The given revision doesn't have a trigger registered matching the input trigger" - ) + if (!triggerDefinition) + throw new AutomateInvalidTriggerError( + "The given revision doesn't have a trigger registered matching the input trigger" + ) - const triggeringVersion = await versionGetter(manifest.versionId) - if (!triggeringVersion) - throw new AutomateInvalidTriggerError('The triggering version is not found') + const triggeringVersion = await versionGetter(manifest.versionId) + if (!triggeringVersion) + throw new AutomateInvalidTriggerError('The triggering version is not found') - const userId = triggeringVersion.author - if (!userId) - throw new AutomateInvalidTriggerError( - "The user, that created the triggering version doesn't exist any more" - ) + const userId = triggeringVersion.author + if (!userId) + throw new AutomateInvalidTriggerError( + "The user, that created the triggering version doesn't exist any more" + ) - const token = await automationTokenGetter(automationWithRevision.id) - if (!token) - throw new AutomateInvalidTriggerError('Cannot find a token for the automation') + const token = await automationTokenGetter(automationWithRevision.id) + if (!token) + throw new AutomateInvalidTriggerError('Cannot find a token for the automation') - return { - automationWithRevision, - userId, - automateToken: token.automateToken - } + return { + automationWithRevision, + userId, + automateToken: token.automateToken } + } async function composeTriggerData(params: { projectId: string @@ -441,64 +446,64 @@ export type ManuallyTriggerAutomationDeps = { export const manuallyTriggerAutomation = (deps: ManuallyTriggerAutomationDeps) => - async (params: { - automationId: string - userId: string - projectId?: string - userResourceAccessRules?: ContextResourceAccessRules - }) => { - const { automationId, userId, projectId, userResourceAccessRules } = params - const { - getAutomationTriggerDefinitions, - getAutomation, - getBranchLatestCommits, - triggerFunction - } = deps - - const [automation, triggerDefs] = await Promise.all([ - getAutomation({ automationId, projectId }), - getAutomationTriggerDefinitions({ - automationId, - projectId, - triggerType: VersionCreationTriggerType - }) - ]) - if (!automation) { - throw new TriggerAutomationError('Automation not found') - } - if (!triggerDefs.length) { - throw new TriggerAutomationError( - 'No model version creation triggers found for the automation' - ) - } - - await validateStreamAccess( - userId, - automation.projectId, - Roles.Stream.Owner, - userResourceAccessRules + async (params: { + automationId: string + userId: string + projectId?: string + userResourceAccessRules?: ContextResourceAccessRules + }) => { + const { automationId, userId, projectId, userResourceAccessRules } = params + const { + getAutomationTriggerDefinitions, + getAutomation, + getBranchLatestCommits, + triggerFunction + } = deps + + const [automation, triggerDefs] = await Promise.all([ + getAutomation({ automationId, projectId }), + getAutomationTriggerDefinitions({ + automationId, + projectId, + triggerType: VersionCreationTriggerType + }) + ]) + if (!automation) { + throw new TriggerAutomationError('Automation not found') + } + if (!triggerDefs.length) { + throw new TriggerAutomationError( + 'No model version creation triggers found for the automation' ) + } - const validModelIds = triggerDefs.map((t) => t.triggeringId) - const [latestCommit] = await getBranchLatestCommits( - validModelIds, - automation.projectId, - { limit: 1 } + await validateStreamAccess( + userId, + automation.projectId, + Roles.Stream.Owner, + userResourceAccessRules + ) + + const validModelIds = triggerDefs.map((t) => t.triggeringId) + const [latestCommit] = await getBranchLatestCommits( + validModelIds, + automation.projectId, + { limit: 1 } + ) + if (!latestCommit) { + throw new TriggerAutomationError( + 'No version to trigger on found for the available triggers' ) - if (!latestCommit) { - throw new TriggerAutomationError( - 'No version to trigger on found for the available triggers' - ) - } - - // Trigger "model version created" - return await triggerFunction({ - revisionId: triggerDefs[0].automationRevisionId, - manifest: { - projectId, - modelId: latestCommit.branchId, - versionId: latestCommit.id, - triggerType: VersionCreationTriggerType - } - }) } + + // Trigger "model version created" + return await triggerFunction({ + revisionId: triggerDefs[0].automationRevisionId, + manifest: { + projectId, + modelId: latestCommit.branchId, + versionId: latestCommit.id, + triggerType: VersionCreationTriggerType + } + }) + } diff --git a/packages/server/modules/automate/tests/trigger.spec.ts b/packages/server/modules/automate/tests/trigger.spec.ts index 1552cff50c..4a0fb1daa7 100644 --- a/packages/server/modules/automate/tests/trigger.spec.ts +++ b/packages/server/modules/automate/tests/trigger.spec.ts @@ -6,7 +6,7 @@ import { triggerAutomationRevisionRun } from '@/modules/automate/services/trigger' import { - AutomateRunStatuses, + AutomationRunStatuses, AutomationTriggerDefinitionRecord, AutomationTriggerType, BaseTriggerManifest, @@ -1011,7 +1011,7 @@ const { FF_AUTOMATE_MODULE_ENABLED } = Environment.getFeatureFlags() automationRevisionId: createdRevision.id, createdAt: new Date(), updatedAt: new Date(), - status: AutomateRunStatuses.running, + status: AutomationRunStatuses.running, executionEngineRunId: cryptoRandomString({ length: 10 }), triggers: [ { @@ -1024,7 +1024,7 @@ const { FF_AUTOMATE_MODULE_ENABLED } = Environment.getFeatureFlags() functionId: generateFunctionId(), functionReleaseId: generateFunctionReleaseId(), id: cryptoRandomString({ length: 15 }), - status: AutomateRunStatuses.running, + status: AutomationRunStatuses.running, elapsed: 0, results: null, contextView: null, @@ -1180,8 +1180,8 @@ const { FF_AUTOMATE_MODULE_ENABLED } = Environment.getFeatureFlags() getFullAutomationRunById(automationRun.id), getFunctionRun(functionRunId) ]) - expect(updatedRun?.status).to.equal(AutomateRunStatuses.success) - expect(updatedFnRun?.status).to.equal(AutomateRunStatuses.success) + expect(updatedRun?.status).to.equal(AutomationRunStatuses.success) + expect(updatedFnRun?.status).to.equal(AutomationRunStatuses.success) expect(updatedFnRun?.contextView).to.equal(contextView) }) }) diff --git a/packages/server/modules/automate/utils/automateFunctionRunStatus.ts b/packages/server/modules/automate/utils/automateFunctionRunStatus.ts index d426a0f412..cec84a4c0d 100644 --- a/packages/server/modules/automate/utils/automateFunctionRunStatus.ts +++ b/packages/server/modules/automate/utils/automateFunctionRunStatus.ts @@ -1,4 +1,7 @@ -import { AutomationRunStatus, AutomationRunStatuses } from "@/modules/automate/helpers/types" +import { + AutomationRunStatus, + AutomationRunStatuses +} from '@/modules/automate/helpers/types' import { FunctionRunReportStatusesError } from '@/modules/automate/errors/runs' import { AutomateRunStatus } from '@/modules/core/graph/generated/graphql' @@ -77,4 +80,4 @@ export const mapDbStatusToGqlStatus = (status: AutomationRunStatus) => { case AutomationRunStatuses.canceled: return AutomateRunStatus.Canceled } -} \ No newline at end of file +} From 403bc0f4e4baaff8cf98e4c33544708d39521ea0 Mon Sep 17 00:00:00 2001 From: Chuck Driesler Date: Wed, 15 May 2024 13:50:05 +0100 Subject: [PATCH 5/9] add all statuses to `AutomationRunStatusOrder` --- .../server/modules/automate/errors/runs.ts | 5 ++++ .../automate/repositories/automations.ts | 4 +-- .../modules/automate/services/trigger.ts | 5 ++-- .../utils/automateFunctionRunStatus.ts | 29 +++++++++---------- 4 files changed, 23 insertions(+), 20 deletions(-) diff --git a/packages/server/modules/automate/errors/runs.ts b/packages/server/modules/automate/errors/runs.ts index 0c89c324ff..7233e2fa6f 100644 --- a/packages/server/modules/automate/errors/runs.ts +++ b/packages/server/modules/automate/errors/runs.ts @@ -1,5 +1,10 @@ import { BaseError } from '@/modules/shared/errors/base' +export class FunctionRunNotFoundError extends BaseError { + static defaultMessage = 'Could not find function run with given id' + static code = 'FUNCTION_RUN_NOT_FOUND' +} + export class FunctionRunReportStatusesError extends BaseError { static defaultMessage = 'An error occurred while updating function run report statuses' diff --git a/packages/server/modules/automate/repositories/automations.ts b/packages/server/modules/automate/repositories/automations.ts index 07c046f52a..086bac5acb 100644 --- a/packages/server/modules/automate/repositories/automations.ts +++ b/packages/server/modules/automate/repositories/automations.ts @@ -93,9 +93,9 @@ export async function getFullAutomationRevisionMetadata( } } -export type InsertableAutomationFunctionRun = Omit< +export type InsertableAutomationFunctionRun = Pick< AutomationFunctionRunRecord, - 'id' | 'runId' + 'id' | 'runId' | 'status' | 'statusMessage' | 'contextView' | 'results' > export async function upsertAutomationFunctionRun( diff --git a/packages/server/modules/automate/services/trigger.ts b/packages/server/modules/automate/services/trigger.ts index f6b7af0336..f926191083 100644 --- a/packages/server/modules/automate/services/trigger.ts +++ b/packages/server/modules/automate/services/trigger.ts @@ -28,7 +28,8 @@ import { DefaultAppIds } from '@/modules/auth/defaultApps' import { Merge } from 'type-fest' import { AutomateInvalidTriggerError, - AutomationFunctionInputEncryptionError + AutomationFunctionInputEncryptionError, + FunctionNotFoundError } from '@/modules/automate/errors/management' import { triggerAutomationRun, @@ -200,7 +201,7 @@ export const setFunctionRunStatusReport = const currentFunctionRunRecord = await getAutomationFunctionRunRecord(runId) if (!currentFunctionRunRecord) { - return false + throw new FunctionNotFoundError() } const currentStatus = currentFunctionRunRecord.status diff --git a/packages/server/modules/automate/utils/automateFunctionRunStatus.ts b/packages/server/modules/automate/utils/automateFunctionRunStatus.ts index cec84a4c0d..479e957e8c 100644 --- a/packages/server/modules/automate/utils/automateFunctionRunStatus.ts +++ b/packages/server/modules/automate/utils/automateFunctionRunStatus.ts @@ -5,15 +5,16 @@ import { import { FunctionRunReportStatusesError } from '@/modules/automate/errors/runs' import { AutomateRunStatus } from '@/modules/core/graph/generated/graphql' -const AutomationRunStatusOrder: Array = [ - AutomationRunStatuses.pending, - AutomationRunStatuses.running, - [ - AutomationRunStatuses.exception, - AutomationRunStatuses.failed, - AutomationRunStatuses.succeeded - ] -] +const AutomationRunStatusOrder: { [key in AutomationRunStatus]: number } = { + pending: 0, + initializing: 1, + running: 2, + succeeded: 3, + failed: 4, + exception: 5, + timeout: 6, + canceled: 7 +} /** * Given a previous and new status, verify that the new status is a valid move. @@ -26,14 +27,10 @@ export const validateStatusChange = ( ): void => { if (previousStatus === newStatus) return - const previousStatusIndex = AutomationRunStatusOrder.findIndex((s) => - Array.isArray(s) ? s.includes(previousStatus) : s === previousStatus - ) - const newStatusIndex = AutomationRunStatusOrder.findIndex((s) => - Array.isArray(s) ? s.includes(newStatus) : s === newStatus - ) + const previousStatusRank = AutomationRunStatusOrder[previousStatus] + const newStatusRank = AutomationRunStatusOrder[newStatus] - if (newStatusIndex <= previousStatusIndex) { + if (newStatusRank <= previousStatusRank) { throw new FunctionRunReportStatusesError( `Invalid status change. Attempting to move from '${previousStatus}' to '${newStatus}'.` ) From 693b76732df7440fbdfc03d47ed06948e0bcea39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gerg=C5=91=20Jedlicska?= Date: Wed, 15 May 2024 17:19:01 +0200 Subject: [PATCH 6/9] fix: status reporting now works --- .../lib/common/generated/gql/graphql.ts | 8 +++-- .../automate/graph/resolvers/automate.ts | 4 ++- .../automate/repositories/automations.ts | 4 ++- .../automate/services/runsManagement.ts | 6 +++- .../modules/automate/services/trigger.ts | 35 +++++++++++++++++-- 5 files changed, 50 insertions(+), 7 deletions(-) diff --git a/packages/frontend-2/lib/common/generated/gql/graphql.ts b/packages/frontend-2/lib/common/generated/gql/graphql.ts index eb0ac248cf..47fbc8d8f9 100644 --- a/packages/frontend-2/lib/common/generated/gql/graphql.ts +++ b/packages/frontend-2/lib/common/generated/gql/graphql.ts @@ -323,10 +323,14 @@ export type AutomateRunCollection = { }; export enum AutomateRunStatus { + Canceled = 'CANCELED', + Exception = 'EXCEPTION', Failed = 'FAILED', Initializing = 'INITIALIZING', + Pending = 'PENDING', Running = 'RUNNING', - Succeeded = 'SUCCEEDED' + Succeeded = 'SUCCEEDED', + Timeout = 'TIMEOUT' } export enum AutomateRunTriggerType { @@ -1307,7 +1311,7 @@ export type MutationAppUpdateArgs = { export type MutationAutomateFunctionRunStatusReportArgs = { - input: Array; + input: AutomateFunctionRunStatusReportInput; }; diff --git a/packages/server/modules/automate/graph/resolvers/automate.ts b/packages/server/modules/automate/graph/resolvers/automate.ts index 2da1f3e4ca..af64ec4bbe 100644 --- a/packages/server/modules/automate/graph/resolvers/automate.ts +++ b/packages/server/modules/automate/graph/resolvers/automate.ts @@ -19,6 +19,7 @@ import { getProjectAutomationsTotalCount, storeAutomation, storeAutomationRevision, + updateAutomationRun, updateAutomation as updateDbAutomation, upsertAutomationFunctionRun } from '@/modules/automate/repositories/automations' @@ -534,7 +535,8 @@ export = { async automateFunctionRunStatusReport(_parent, { input }) { const deps: SetFunctionRunStatusReportDeps = { getAutomationFunctionRunRecord: getFunctionRun, - upsertAutomationFunctionRunRecord: upsertAutomationFunctionRun + upsertAutomationFunctionRunRecord: upsertAutomationFunctionRun, + automationRunUpdater: updateAutomationRun } const payload = { diff --git a/packages/server/modules/automate/repositories/automations.ts b/packages/server/modules/automate/repositories/automations.ts index 086bac5acb..bef228fb26 100644 --- a/packages/server/modules/automate/repositories/automations.ts +++ b/packages/server/modules/automate/repositories/automations.ts @@ -169,7 +169,7 @@ export async function getFunctionRuns(params: { functionRunIds: string[] }) { AutomationRuns.col.automationRevisionId, AutomationRevisions.col.automationId ]) - .whereIn(AutomationFunctionRuns.col.id, functionRunIds) + .whereIn(AutomationFunctionRuns.col.runId, functionRunIds) .innerJoin( AutomationRuns.name, AutomationRuns.col.id, @@ -185,6 +185,8 @@ export async function getFunctionRuns(params: { functionRunIds: string[] }) { } export async function getFunctionRun(functionRunId: string) { + // TODO, make sure we're also doing the same joins as above + // const run = AutomationFunctionRuns.knex().select() const runs = await getFunctionRuns({ functionRunIds: [functionRunId] }) return (runs[0] || null) as (typeof runs)[0] | null } diff --git a/packages/server/modules/automate/services/runsManagement.ts b/packages/server/modules/automate/services/runsManagement.ts index c864ef923f..36748de501 100644 --- a/packages/server/modules/automate/services/runsManagement.ts +++ b/packages/server/modules/automate/services/runsManagement.ts @@ -20,7 +20,7 @@ import { AutomateFunctionRunStatusReportInput } from '@/modules/core/graph/gener import { Automate } from '@speckle/shared' import { difference, groupBy, keyBy, mapValues, reduce, uniqBy } from 'lodash' -const validateContextView = (contextView: string) => { +export const validateContextView = (contextView: string) => { if (!contextView.length) { throw new FunctionRunReportStatusesError( 'Context view must be a valid relative URL' @@ -124,6 +124,7 @@ export const reportFunctionRunStatuses = if (input.results) { try { + // TODO: take this, its validating, that the input matches any know schema Automate.AutomateTypes.formatResultsSchema(input.results) } catch (e) { if (e instanceof Automate.UnformattableResultsSchemaError) { @@ -137,6 +138,7 @@ export const reportFunctionRunStatuses = if (input.contextView) { try { + // TODO: take this, its validating the context view to be a correct url validateContextView(input.contextView) } catch (e) { if (e instanceof FunctionRunReportStatusesError) { @@ -196,6 +198,7 @@ export const reportFunctionRunStatuses = ) // Update automation run + // TODO: take this update, and do calc const updatedRun = await updateAutomationRun({ id: runId, status: newAutomationStatus, @@ -211,6 +214,7 @@ export const reportFunctionRunStatuses = ) ] + // TODO: take the even emitter await AutomateRunsEmitter.emit(AutomateRunsEmitter.events.StatusUpdated, { run: updatedRun, functionRuns: allFnRuns, diff --git a/packages/server/modules/automate/services/trigger.ts b/packages/server/modules/automate/services/trigger.ts index f926191083..7881c8f488 100644 --- a/packages/server/modules/automate/services/trigger.ts +++ b/packages/server/modules/automate/services/trigger.ts @@ -7,7 +7,8 @@ import { getAutomationTriggerDefinitions, upsertAutomationRun, upsertAutomationFunctionRun, - getFunctionRun + getFunctionRun, + updateAutomationRun } from '@/modules/automate/repositories/automations' import { AutomationWithRevision, @@ -47,6 +48,8 @@ import { import { LibsodiumEncryptionError } from '@/modules/shared/errors/encryption' import { AutomateRunsEmitter } from '@/modules/automate/events/runs' import { validateStatusChange } from '@/modules/automate/utils/automateFunctionRunStatus' +import { Automate } from '@speckle/shared' +import { validateContextView } from '@/modules/automate/services/runsManagement' export type OnModelVersionCreateDeps = { getTriggers: typeof getActiveTriggerDefinitions @@ -185,8 +188,10 @@ const createAutomationRunData = export type SetFunctionRunStatusReportDeps = { getAutomationFunctionRunRecord: typeof getFunctionRun upsertAutomationFunctionRunRecord: typeof upsertAutomationFunctionRun + automationRunUpdater: typeof updateAutomationRun } +//TODO: lets move this to runsManagement.ts export const setFunctionRunStatusReport = (deps: SetFunctionRunStatusReportDeps) => async ( @@ -195,7 +200,11 @@ export const setFunctionRunStatusReport = 'runId' | 'status' | 'statusMessage' | 'contextView' | 'results' > ): Promise => { - const { getAutomationFunctionRunRecord, upsertAutomationFunctionRunRecord } = deps + const { + getAutomationFunctionRunRecord, + upsertAutomationFunctionRunRecord, + automationRunUpdater + } = deps const { runId, ...statusReportData } = params const currentFunctionRunRecord = await getAutomationFunctionRunRecord(runId) @@ -204,6 +213,13 @@ export const setFunctionRunStatusReport = throw new FunctionNotFoundError() } + if (statusReportData.results) { + console.log(statusReportData.results) + Automate.AutomateTypes.formatResultsSchema(statusReportData.results) + } + + if (statusReportData.contextView) validateContextView(statusReportData.contextView) + const currentStatus = currentFunctionRunRecord.status const nextStatus = statusReportData.status @@ -219,6 +235,21 @@ export const setFunctionRunStatusReport = await upsertAutomationFunctionRunRecord(nextFunctionRunRecord) + // TODO: this needs to change when we add support for multiple functions in a run + // we need to calculate the final status + const newAutomationStatus = statusReportData.status + + const updatedRun = await automationRunUpdater({ + id: runId, + status: newAutomationStatus, + updatedAt: new Date() + }) + + await AutomateRunsEmitter.emit(AutomateRunsEmitter.events.StatusUpdated, { + run: updatedRun, + functionRuns: [nextFunctionRunRecord], + automationId: currentFunctionRunRecord.automationId + }) return true } From 4bdf554fe7afb8b11c7843b48de2526f6377f494 Mon Sep 17 00:00:00 2001 From: Chuck Driesler Date: Wed, 15 May 2024 17:43:31 +0100 Subject: [PATCH 7/9] park in the right place, grapple with tests --- .../automate/graph/resolvers/automate.ts | 12 +- .../automate/repositories/automations.ts | 16 +- .../automate/services/automationManagement.ts | 12 +- .../automate/services/runsManagement.ts | 290 ++++++------------ .../modules/automate/services/trigger.ts | 82 +---- .../modules/automate/tests/trigger.spec.ts | 141 ++++----- .../utils/automateFunctionRunStatus.ts | 33 -- packages/server/package.json | 2 + packages/server/test/hooks.js | 2 + yarn.lock | 29 ++ 10 files changed, 209 insertions(+), 410 deletions(-) diff --git a/packages/server/modules/automate/graph/resolvers/automate.ts b/packages/server/modules/automate/graph/resolvers/automate.ts index af64ec4bbe..a0d77782af 100644 --- a/packages/server/modules/automate/graph/resolvers/automate.ts +++ b/packages/server/modules/automate/graph/resolvers/automate.ts @@ -53,11 +53,13 @@ import { getBranchesByIds } from '@/modules/core/repositories/branches' import { - setFunctionRunStatusReport, manuallyTriggerAutomation, - triggerAutomationRevisionRun, - SetFunctionRunStatusReportDeps + triggerAutomationRevisionRun } from '@/modules/automate/services/trigger' +import { + reportFunctionRunStatus, + ReportFunctionRunStatusDeps +} from '@/modules/automate/services/runsManagement' import { AutomateFunctionReleaseNotFoundError, FunctionNotFoundError @@ -533,7 +535,7 @@ export = { }, Mutation: { async automateFunctionRunStatusReport(_parent, { input }) { - const deps: SetFunctionRunStatusReportDeps = { + const deps: ReportFunctionRunStatusDeps = { getAutomationFunctionRunRecord: getFunctionRun, upsertAutomationFunctionRunRecord: upsertAutomationFunctionRun, automationRunUpdater: updateAutomationRun @@ -548,7 +550,7 @@ export = { statusMessage: input.statusMessage ?? null } - const result = await setFunctionRunStatusReport(deps)(payload) + const result = await reportFunctionRunStatus(deps)(payload) return result }, diff --git a/packages/server/modules/automate/repositories/automations.ts b/packages/server/modules/automate/repositories/automations.ts index bef228fb26..56b5d4c8df 100644 --- a/packages/server/modules/automate/repositories/automations.ts +++ b/packages/server/modules/automate/repositories/automations.ts @@ -152,10 +152,7 @@ export async function upsertAutomationRun(automationRun: InsertableAutomationRun return } -export async function getFunctionRuns(params: { functionRunIds: string[] }) { - const { functionRunIds } = params - if (!functionRunIds.length) return [] - +export async function getFunctionRun(functionRunId: string) { const q = AutomationFunctionRuns.knex() .select< Array< @@ -169,7 +166,7 @@ export async function getFunctionRuns(params: { functionRunIds: string[] }) { AutomationRuns.col.automationRevisionId, AutomationRevisions.col.automationId ]) - .whereIn(AutomationFunctionRuns.col.runId, functionRunIds) + .where(AutomationFunctionRuns.col.runId, functionRunId) .innerJoin( AutomationRuns.name, AutomationRuns.col.id, @@ -181,14 +178,9 @@ export async function getFunctionRuns(params: { functionRunIds: string[] }) { AutomationRuns.col.automationRevisionId ) - return await q -} + const runs = await q -export async function getFunctionRun(functionRunId: string) { - // TODO, make sure we're also doing the same joins as above - // const run = AutomationFunctionRuns.knex().select() - const runs = await getFunctionRuns({ functionRunIds: [functionRunId] }) - return (runs[0] || null) as (typeof runs)[0] | null + return (runs[0] ?? null) as (typeof runs)[0] | null } export type GetFunctionRunsForAutomationRunIdsItem = AutomationFunctionRunRecord & { diff --git a/packages/server/modules/automate/services/automationManagement.ts b/packages/server/modules/automate/services/automationManagement.ts index 66cf17a13f..03784a2a3c 100644 --- a/packages/server/modules/automate/services/automationManagement.ts +++ b/packages/server/modules/automate/services/automationManagement.ts @@ -442,8 +442,8 @@ export const getAutomationsStatus = const failedAutomations = runsWithUpdatedStatus.filter( (a) => - a.status === AutomationRunStatuses.failure || - a.status === AutomationRunStatuses.error + a.status === AutomationRunStatuses.failed || + a.status === AutomationRunStatuses.exception ) const runningAutomations = runsWithUpdatedStatus.filter( @@ -453,17 +453,17 @@ export const getAutomationsStatus = (a) => a.status === AutomationRunStatuses.pending ) - let status = AutomationRunStatuses.success + let status = AutomationRunStatuses.succeeded let statusMessage = 'All automations have succeeded' if (failedAutomations.length) { - status = AutomationRunStatuses.failure + status = AutomationRunStatuses.failed statusMessage = 'Some automations have failed:' for (const fa of failedAutomations) { for (const functionRunStatus of fa.functionRuns) { if ( - functionRunStatus.status === AutomationRunStatuses.failure || - functionRunStatus.status === AutomationRunStatuses.error + functionRunStatus.status === AutomationRunStatuses.failed || + functionRunStatus.status === AutomationRunStatuses.exception ) statusMessage += `\n${functionRunStatus.statusMessage}` } diff --git a/packages/server/modules/automate/services/runsManagement.ts b/packages/server/modules/automate/services/runsManagement.ts index 36748de501..899065d9f1 100644 --- a/packages/server/modules/automate/services/runsManagement.ts +++ b/packages/server/modules/automate/services/runsManagement.ts @@ -1,24 +1,51 @@ -import { automateLogger } from '@/logging/logging' import { FunctionRunReportStatusesError } from '@/modules/automate/errors/runs' +import { FunctionNotFoundError } from '@/modules/automate/errors/management' import { AutomateRunsEmitter } from '@/modules/automate/events/runs' import { + AutomationFunctionRunRecord, AutomationRunStatus, AutomationRunStatuses } from '@/modules/automate/helpers/types' import { - GetFunctionRunsForAutomationRunIdsItem, - getFunctionRuns, - getFunctionRunsForAutomationRunIds, + getFunctionRun, updateAutomationRun, - updateFunctionRun + upsertAutomationFunctionRun } from '@/modules/automate/repositories/automations' -import { - mapGqlStatusToDbStatus, - validateStatusChange -} from '@/modules/automate/utils/automateFunctionRunStatus' -import { AutomateFunctionRunStatusReportInput } from '@/modules/core/graph/generated/graphql' import { Automate } from '@speckle/shared' -import { difference, groupBy, keyBy, mapValues, reduce, uniqBy } from 'lodash' + +const AutomationRunStatusOrder: { [key in AutomationRunStatus]: number } = { + pending: 0, + initializing: 1, + running: 2, + succeeded: 3, + failed: 4, + exception: 5, + timeout: 6, + canceled: 7 +} + +/** + * Given a previous and new status, verify that the new status is a valid move. + * @remarks This is to protect against race conditions that may report "backwards" motion + * in function statuses. (i.e. `FAILED` => `RUNNING`) + */ +export const validateStatusChange = ( + previousStatus: AutomationRunStatus, + newStatus: AutomationRunStatus +): void => { + console.log(`${previousStatus} => ${newStatus}`) + + if (previousStatus === newStatus) return + + const previousStatusRank = AutomationRunStatusOrder[previousStatus] + const newStatusRank = AutomationRunStatusOrder[newStatus] + + if (newStatusRank <= previousStatusRank) { + throw new FunctionRunReportStatusesError( + `Invalid status change. Attempting to move from '${previousStatus}' to '${newStatus}'.` + ) + } +} export const validateContextView = (contextView: string) => { if (!contextView.length) { @@ -41,207 +68,86 @@ export const validateContextView = (contextView: string) => { } } -type ValidatedRunStatusUpdateItem = { - update: AutomateFunctionRunStatusReportInput - run: Awaited>[0] - newStatus: AutomationRunStatus -} - export const resolveStatusFromFunctionRunStatuses = ( functionRunStatuses: AutomationRunStatus[] ) => { - const anyPending = functionRunStatuses.includes(AutomationRunStatuses.pending) + const anyPending = + functionRunStatuses.includes(AutomationRunStatuses.pending) || + functionRunStatuses.includes(AutomationRunStatuses.initializing) if (anyPending) return AutomationRunStatuses.pending const anyRunning = functionRunStatuses.includes(AutomationRunStatuses.running) if (anyRunning) return AutomationRunStatuses.running - const anyError = functionRunStatuses.includes(AutomationRunStatuses.exception) - if (anyError) return AutomationRunStatuses.exception - const anyFailure = functionRunStatuses.includes(AutomationRunStatuses.failed) if (anyFailure) return AutomationRunStatuses.failed + const anyError = functionRunStatuses.includes(AutomationRunStatuses.exception) + if (anyError) return AutomationRunStatuses.exception + return AutomationRunStatuses.succeeded } -export type ReportFunctionRunStatusesDeps = { - getFunctionRuns: typeof getFunctionRuns - updateFunctionRun: typeof updateFunctionRun - updateAutomationRun: typeof updateAutomationRun - getFunctionRunsForAutomationRunIds: typeof getFunctionRunsForAutomationRunIds +export type ReportFunctionRunStatusDeps = { + getAutomationFunctionRunRecord: typeof getFunctionRun + upsertAutomationFunctionRunRecord: typeof upsertAutomationFunctionRun + automationRunUpdater: typeof updateAutomationRun } -export const reportFunctionRunStatuses = - (deps: ReportFunctionRunStatusesDeps) => - async (params: { inputs: AutomateFunctionRunStatusReportInput[] }) => { - const { inputs } = params - const { getFunctionRuns, updateFunctionRun, updateAutomationRun } = deps - - const uniqueInputs = uniqBy(inputs, (i) => i.functionRunId) - const updatableFunctionRunIds = uniqueInputs.map((i) => i.functionRunId) - const existingRuns = keyBy( - await getFunctionRuns({ - functionRunIds: updatableFunctionRunIds - }), - (r) => r.id - ) - const allAutomationRunFunctionRuns = reduce( - await getFunctionRunsForAutomationRunIds({ - functionRunIds: updatableFunctionRunIds - }), - (acc, r) => { - acc[r.runId] = acc[r.runId] || {} - acc[r.runId][r.id] = r - return acc - }, - {} as Record> - ) - - const errorsByRunId: Record = {} - const validatedUpdates: Array = [] - for (const input of uniqueInputs) { - const run = existingRuns[input.functionRunId] - if (!run) { - errorsByRunId[input.functionRunId] = `Function run not found` - continue - } - - const newStatus = mapGqlStatusToDbStatus(input.status) - - try { - validateStatusChange(run.status, newStatus) - } catch (e) { - if (e instanceof FunctionRunReportStatusesError) { - errorsByRunId[ - input.functionRunId - ] = `Invalid status change for function run: ${e.message}` - continue - } else { - throw e - } - } - - if (input.results) { - try { - // TODO: take this, its validating, that the input matches any know schema - Automate.AutomateTypes.formatResultsSchema(input.results) - } catch (e) { - if (e instanceof Automate.UnformattableResultsSchemaError) { - errorsByRunId[input.functionRunId] = `Invalid results schema: ${e.message}` - continue - } else { - throw e - } - } - } - - if (input.contextView) { - try { - // TODO: take this, its validating the context view to be a correct url - validateContextView(input.contextView) - } catch (e) { - if (e instanceof FunctionRunReportStatusesError) { - errorsByRunId[input.functionRunId] = `Invalid contextView: ${e.message}` - continue - } else { - throw e - } - } - } - - validatedUpdates.push({ update: input, run, newStatus }) +export const reportFunctionRunStatus = + (deps: ReportFunctionRunStatusDeps) => + async ( + params: Pick< + AutomationFunctionRunRecord, + 'runId' | 'status' | 'statusMessage' | 'contextView' | 'results' + > + ): Promise => { + const { + getAutomationFunctionRunRecord, + upsertAutomationFunctionRunRecord, + automationRunUpdater + } = deps + const { runId, ...statusReportData } = params + + const currentFunctionRunRecord = await getAutomationFunctionRunRecord(runId) + + if (!currentFunctionRunRecord) { + throw new FunctionNotFoundError() } - // Group by automation run - const groupedRuns = groupBy(validatedUpdates, (r) => r.run.runId) - for (const [runId, updates] of Object.entries(groupedRuns)) { - if (!updates.length) continue - - try { - // Taking all function run statuses into account when calculating new automation status, - // even function run statuses that were not updated in this call - const preexistingFunctionRuns = allAutomationRunFunctionRuns[runId] || {} - const finalFunctionRunStatuses = { - ...mapValues(preexistingFunctionRuns, (r) => r.status), - ...reduce( - updates, - (acc, u) => { - acc[u.update.functionRunId] = u.newStatus - return acc - }, - {} as Record - ) - } - - const newAutomationStatus = resolveStatusFromFunctionRunStatuses( - Object.values(finalFunctionRunStatuses) - ) - - // Update function runs - const updatedFnRuns = await Promise.all( - updates.map((u) => - updateFunctionRun({ - id: u.update.functionRunId, - status: u.newStatus, - ...(u.update.contextView?.length - ? { contextView: u.update.contextView } - : {}), - ...(u.update.results - ? { results: u.update.results as Automate.AutomateTypes.ResultsSchema } - : {}), - ...(u.update.statusMessage?.length - ? { statusMessage: u.update.statusMessage } - : {}) - }) - ) - ) - - // Update automation run - // TODO: take this update, and do calc - const updatedRun = await updateAutomationRun({ - id: runId, - status: newAutomationStatus, - updatedAt: new Date() - }) - - // Collect all function runs together in one array - const updatedRunIds = updatedFnRuns.map((r) => r.id) - const allFnRuns: typeof updatedFnRuns = [ - ...updatedFnRuns, - ...Object.values(preexistingFunctionRuns).filter( - (r) => !updatedRunIds.includes(r.id) - ) - ] - - // TODO: take the even emitter - await AutomateRunsEmitter.emit(AutomateRunsEmitter.events.StatusUpdated, { - run: updatedRun, - functionRuns: allFnRuns, - automationId: updates[0].run.automationId - }) - } catch (e) { - automateLogger.error('Automation run status update failed', e, { - runId, - updates - }) - - for (const update of updates) { - errorsByRunId[ - update.update.functionRunId - ] = `Unexpectedly failed to update status` - } - continue - } + if (statusReportData.results) { + console.log(statusReportData.results) + Automate.AutomateTypes.formatResultsSchema(statusReportData.results) } - const successfulUpdates = difference( - validatedUpdates.map((u) => u.update.functionRunId), - Object.keys(errorsByRunId) - ) + if (statusReportData.contextView) validateContextView(statusReportData.contextView) + + const currentStatus = currentFunctionRunRecord.status + const nextStatus = statusReportData.status + + validateStatusChange(currentStatus, nextStatus) + + const elapsed = new Date().getTime() - currentFunctionRunRecord.createdAt.getTime() - return { - successfullyUpdatedFunctionRunIds: successfulUpdates, - errorsByFunctionRunId: errorsByRunId + const nextFunctionRunRecord: AutomationFunctionRunRecord = { + ...currentFunctionRunRecord, + ...statusReportData, + elapsed } + + await upsertAutomationFunctionRunRecord(nextFunctionRunRecord) + + const updatedRun = await automationRunUpdater({ + id: runId, + status: resolveStatusFromFunctionRunStatuses([nextStatus]), + updatedAt: new Date() + }) + + await AutomateRunsEmitter.emit(AutomateRunsEmitter.events.StatusUpdated, { + run: updatedRun, + functionRuns: [nextFunctionRunRecord], + automationId: currentFunctionRunRecord.automationId + }) + + return true } diff --git a/packages/server/modules/automate/services/trigger.ts b/packages/server/modules/automate/services/trigger.ts index 7881c8f488..232103a854 100644 --- a/packages/server/modules/automate/services/trigger.ts +++ b/packages/server/modules/automate/services/trigger.ts @@ -5,10 +5,7 @@ import { getFullAutomationRevisionMetadata, getAutomationToken, getAutomationTriggerDefinitions, - upsertAutomationRun, - upsertAutomationFunctionRun, - getFunctionRun, - updateAutomationRun + upsertAutomationRun } from '@/modules/automate/repositories/automations' import { AutomationWithRevision, @@ -17,8 +14,7 @@ import { VersionCreatedTriggerManifest, VersionCreationTriggerType, BaseTriggerManifest, - isVersionCreatedTriggerManifest, - AutomationFunctionRunRecord + isVersionCreatedTriggerManifest } from '@/modules/automate/helpers/types' import { getBranchLatestCommits } from '@/modules/core/repositories/branches' import { getCommit } from '@/modules/core/repositories/commits' @@ -29,8 +25,7 @@ import { DefaultAppIds } from '@/modules/auth/defaultApps' import { Merge } from 'type-fest' import { AutomateInvalidTriggerError, - AutomationFunctionInputEncryptionError, - FunctionNotFoundError + AutomationFunctionInputEncryptionError } from '@/modules/automate/errors/management' import { triggerAutomationRun, @@ -47,9 +42,6 @@ import { } from '@/modules/automate/services/encryption' import { LibsodiumEncryptionError } from '@/modules/shared/errors/encryption' import { AutomateRunsEmitter } from '@/modules/automate/events/runs' -import { validateStatusChange } from '@/modules/automate/utils/automateFunctionRunStatus' -import { Automate } from '@speckle/shared' -import { validateContextView } from '@/modules/automate/services/runsManagement' export type OnModelVersionCreateDeps = { getTriggers: typeof getActiveTriggerDefinitions @@ -185,74 +177,6 @@ const createAutomationRunData = return automationRun } -export type SetFunctionRunStatusReportDeps = { - getAutomationFunctionRunRecord: typeof getFunctionRun - upsertAutomationFunctionRunRecord: typeof upsertAutomationFunctionRun - automationRunUpdater: typeof updateAutomationRun -} - -//TODO: lets move this to runsManagement.ts -export const setFunctionRunStatusReport = - (deps: SetFunctionRunStatusReportDeps) => - async ( - params: Pick< - AutomationFunctionRunRecord, - 'runId' | 'status' | 'statusMessage' | 'contextView' | 'results' - > - ): Promise => { - const { - getAutomationFunctionRunRecord, - upsertAutomationFunctionRunRecord, - automationRunUpdater - } = deps - const { runId, ...statusReportData } = params - - const currentFunctionRunRecord = await getAutomationFunctionRunRecord(runId) - - if (!currentFunctionRunRecord) { - throw new FunctionNotFoundError() - } - - if (statusReportData.results) { - console.log(statusReportData.results) - Automate.AutomateTypes.formatResultsSchema(statusReportData.results) - } - - if (statusReportData.contextView) validateContextView(statusReportData.contextView) - - const currentStatus = currentFunctionRunRecord.status - const nextStatus = statusReportData.status - - validateStatusChange(currentStatus, nextStatus) - - const elapsed = new Date().getTime() - currentFunctionRunRecord.createdAt.getTime() - - const nextFunctionRunRecord: AutomationFunctionRunRecord = { - ...currentFunctionRunRecord, - ...statusReportData, - elapsed - } - - await upsertAutomationFunctionRunRecord(nextFunctionRunRecord) - - // TODO: this needs to change when we add support for multiple functions in a run - // we need to calculate the final status - const newAutomationStatus = statusReportData.status - - const updatedRun = await automationRunUpdater({ - id: runId, - status: newAutomationStatus, - updatedAt: new Date() - }) - - await AutomateRunsEmitter.emit(AutomateRunsEmitter.events.StatusUpdated, { - run: updatedRun, - functionRuns: [nextFunctionRunRecord], - automationId: currentFunctionRunRecord.automationId - }) - return true - } - export type TriggerAutomationRevisionRunDeps = { automateRunTrigger: typeof triggerAutomationRun } & CreateAutomationRunDataDeps diff --git a/packages/server/modules/automate/tests/trigger.spec.ts b/packages/server/modules/automate/tests/trigger.spec.ts index 4a0fb1daa7..a1970d5253 100644 --- a/packages/server/modules/automate/tests/trigger.spec.ts +++ b/packages/server/modules/automate/tests/trigger.spec.ts @@ -29,15 +29,13 @@ import { getFullAutomationRunById, getAutomationTriggerDefinitions, getFunctionRun, - getFunctionRuns, - getFunctionRunsForAutomationRunIds, storeAutomation, storeAutomationRevision, updateAutomation, updateAutomationRevision, updateAutomationRun, - updateFunctionRun, - upsertAutomationRun + upsertAutomationRun, + upsertAutomationFunctionRun } from '@/modules/automate/repositories/automations' import { beforeEachContext, truncateTables } from '@/test/hooks' import { Automate, Environment } from '@speckle/shared' @@ -54,7 +52,7 @@ import { import { expectToThrow } from '@/test/assertionHelper' import { Commits } from '@/modules/core/dbSchema' import { BranchRecord } from '@/modules/core/helpers/types' -import { reportFunctionRunStatuses } from '@/modules/automate/services/runsManagement' +import { reportFunctionRunStatus } from '@/modules/automate/services/runsManagement' import { AutomateRunStatus } from '@/modules/core/graph/generated/graphql' import { getEncryptionKeyPairFor, @@ -62,6 +60,7 @@ import { getFunctionInputDecryptor } from '@/modules/automate/services/encryption' import { buildDecryptor } from '@/modules/shared/utils/libsodium' +import { mapGqlStatusToDbStatus } from '@/modules/automate/utils/automateFunctionRunStatus' const { FF_AUTOMATE_MODULE_ENABLED } = Environment.getFeatureFlags() @@ -1039,55 +1038,44 @@ const { FF_AUTOMATE_MODULE_ENABLED } = Environment.getFeatureFlags() }) describe('status update report', () => { - const buildReportFunctionRunStatuses = () => { - const report = reportFunctionRunStatuses({ - getFunctionRuns, - updateFunctionRun, - updateAutomationRun, - getFunctionRunsForAutomationRunIds + const buildReportFunctionRunStatus = () => { + const report = reportFunctionRunStatus({ + getAutomationFunctionRunRecord: getFunctionRun, + upsertAutomationFunctionRunRecord: upsertAutomationFunctionRun, + automationRunUpdater: updateAutomationRun }) return report } it('fails fn with invalid functionRunId', async () => { - const report = buildReportFunctionRunStatuses() + const report = buildReportFunctionRunStatus() const functionRunId = 'nonexistent' - const res = await report({ - inputs: [ - { - functionRunId, - status: AutomateRunStatus.Succeeded - } - ] - }) + const params: Parameters[0] = { + runId: functionRunId, + status: mapGqlStatusToDbStatus(AutomateRunStatus.Succeeded), + statusMessage: null, + results: null, + contextView: null + } - expect(res).to.be.ok - expect(res.successfullyUpdatedFunctionRunIds).to.have.length(0) - expect(res.errorsByFunctionRunId[functionRunId]).to.match( - /^Function run not found/ - ) + expect(report(params)).to.eventually.be.rejectedWith('Function not found') }) it('fails fn with invalid status', async () => { - const report = buildReportFunctionRunStatuses() + const report = buildReportFunctionRunStatus() const functionRunId = automationRun.functionRuns[0].id - const res = await report({ - inputs: [ - { - functionRunId, - status: AutomateRunStatus.Initializing - } - ] - }) + const params: Parameters[0] = { + runId: functionRunId, + status: mapGqlStatusToDbStatus(AutomateRunStatus.Pending), + statusMessage: null, + results: null, + contextView: null + } - expect(res).to.be.ok - expect(res.successfullyUpdatedFunctionRunIds).to.have.length(0) - expect(res.errorsByFunctionRunId[functionRunId]).to.match( - /^Invalid status change/ - ) + expect(report(params)).to.eventually.be.rejectedWith('Invalid status change') }) ;[ { val: 1, error: 'invalid type' }, @@ -1115,73 +1103,60 @@ const { FF_AUTOMATE_MODULE_ENABLED } = Environment.getFeatureFlags() } ].forEach(({ val, error }) => { it('fails fn with invalid results: ' + error, async () => { - const report = buildReportFunctionRunStatuses() + const report = buildReportFunctionRunStatus() const functionRunId = automationRun.functionRuns[0].id - const res = await report({ - inputs: [ - { - functionRunId, - status: AutomateRunStatus.Succeeded, - results: val as unknown as Automate.AutomateTypes.ResultsSchema - } - ] - }) + const params: Parameters[0] = { + runId: functionRunId, + status: mapGqlStatusToDbStatus(AutomateRunStatus.Succeeded), + statusMessage: null, + results: val as unknown as Automate.AutomateTypes.ResultsSchema, + contextView: null + } - expect(res).to.be.ok - expect(res.successfullyUpdatedFunctionRunIds).to.have.length(0) - expect(res.errorsByFunctionRunId[functionRunId]).to.match( - /^Invalid results schema/ + expect(report(params)).to.eventually.be.rejectedWith( + 'Invalid results schema' ) }) }) it('fails fn with invalid contextView url', async () => { - const report = buildReportFunctionRunStatuses() + const report = buildReportFunctionRunStatus() const functionRunId = automationRun.functionRuns[0].id - const res = await report({ - inputs: [ - { - functionRunId, - status: AutomateRunStatus.Succeeded, - contextView: 'invalid-url' - } - ] - }) + const params: Parameters[0] = { + runId: functionRunId, + status: mapGqlStatusToDbStatus(AutomateRunStatus.Succeeded), + statusMessage: null, + results: null, + contextView: 'invalid-url' + } - expect(res).to.be.ok - expect(res.successfullyUpdatedFunctionRunIds).to.have.length(0) - expect(res.errorsByFunctionRunId[functionRunId]).to.match( - /^Invalid contextView/ - ) + expect(report(params)).to.eventually.be.rejectedWith('Invalid contextView') }) it('succeeds', async () => { - const report = buildReportFunctionRunStatuses() + const report = buildReportFunctionRunStatus() const functionRunId = automationRun.functionRuns[0].id const contextView = '/a/b/c' - const res = await report({ - inputs: [ - { - functionRunId, - status: AutomateRunStatus.Succeeded, - contextView - } - ] - }) + const params: Parameters[0] = { + runId: functionRunId, + status: mapGqlStatusToDbStatus(AutomateRunStatus.Succeeded), + statusMessage: null, + results: null, + contextView + } - expect(res).to.be.ok - expect(res.successfullyUpdatedFunctionRunIds).to.have.length(1) - expect(Object.values(res.errorsByFunctionRunId)).to.be.empty + expect(report(params)).to.eventually.be.true const [updatedRun, updatedFnRun] = await Promise.all([ getFullAutomationRunById(automationRun.id), getFunctionRun(functionRunId) ]) - expect(updatedRun?.status).to.equal(AutomationRunStatuses.success) - expect(updatedFnRun?.status).to.equal(AutomationRunStatuses.success) + + expect(updatedRun?.status).to.equal(AutomationRunStatuses.succeeded) + expect(updatedFnRun?.status).to.equal(AutomationRunStatuses.succeeded) expect(updatedFnRun?.contextView).to.equal(contextView) }) }) diff --git a/packages/server/modules/automate/utils/automateFunctionRunStatus.ts b/packages/server/modules/automate/utils/automateFunctionRunStatus.ts index 479e957e8c..86b1c08333 100644 --- a/packages/server/modules/automate/utils/automateFunctionRunStatus.ts +++ b/packages/server/modules/automate/utils/automateFunctionRunStatus.ts @@ -2,41 +2,8 @@ import { AutomationRunStatus, AutomationRunStatuses } from '@/modules/automate/helpers/types' -import { FunctionRunReportStatusesError } from '@/modules/automate/errors/runs' import { AutomateRunStatus } from '@/modules/core/graph/generated/graphql' -const AutomationRunStatusOrder: { [key in AutomationRunStatus]: number } = { - pending: 0, - initializing: 1, - running: 2, - succeeded: 3, - failed: 4, - exception: 5, - timeout: 6, - canceled: 7 -} - -/** - * Given a previous and new status, verify that the new status is a valid move. - * @remarks This is to protect against race conditions that may report "backwards" motion - * in function statuses. (i.e. `FAILED` => `RUNNING`) - */ -export const validateStatusChange = ( - previousStatus: AutomationRunStatus, - newStatus: AutomationRunStatus -): void => { - if (previousStatus === newStatus) return - - const previousStatusRank = AutomationRunStatusOrder[previousStatus] - const newStatusRank = AutomationRunStatusOrder[newStatus] - - if (newStatusRank <= previousStatusRank) { - throw new FunctionRunReportStatusesError( - `Invalid status change. Attempting to move from '${previousStatus}' to '${newStatus}'.` - ) - } -} - export const mapGqlStatusToDbStatus = (status: AutomateRunStatus) => { switch (status) { case AutomateRunStatus.Pending: diff --git a/packages/server/package.json b/packages/server/package.json index 1bc982d750..6fce03aeba 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -123,6 +123,7 @@ "@tiptap/core": "^2.0.0-beta.176", "@types/bcrypt": "^5.0.0", "@types/bull": "^3.15.9", + "@types/chai-as-promised": "^7.1.8", "@types/compression": "^1.7.2", "@types/cookie-parser": "^1.4.7", "@types/debug": "^4.1.7", @@ -149,6 +150,7 @@ "@typescript-eslint/parser": "^5.39.0", "axios": "^1.6.0", "chai": "^4.2.0", + "chai-as-promised": "^7.1.2", "chai-http": "^4.3.0", "concurrently": "^7.0.0", "cross-env": "^7.0.3", diff --git a/packages/server/test/hooks.js b/packages/server/test/hooks.js index 0cb106ac19..1a4ab6cfd9 100644 --- a/packages/server/test/hooks.js +++ b/packages/server/test/hooks.js @@ -4,6 +4,7 @@ require('../bootstrap') require('@/test/mocks/global') const chai = require('chai') +const chaiAsPromised = require('chai-as-promised') const chaiHttp = require('chai-http') const deepEqualInAnyOrder = require('deep-equal-in-any-order') const knex = require(`@/db/knex`) @@ -12,6 +13,7 @@ const { default: graphqlChaiPlugin } = require('@/test/plugins/graphql') const { logger } = require('@/logging/logging') // Register chai plugins +chai.use(chaiAsPromised) chai.use(chaiHttp) chai.use(deepEqualInAnyOrder) chai.use(graphqlChaiPlugin) diff --git a/yarn.lock b/yarn.lock index 2655313787..f6f57d7aa1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -14009,6 +14009,7 @@ __metadata: "@tiptap/core": ^2.0.0-beta.176 "@types/bcrypt": ^5.0.0 "@types/bull": ^3.15.9 + "@types/chai-as-promised": ^7.1.8 "@types/compression": ^1.7.2 "@types/cookie-parser": ^1.4.7 "@types/debug": ^4.1.7 @@ -14043,6 +14044,7 @@ __metadata: bull: ^4.8.5 busboy: ^1.4.0 chai: ^4.2.0 + chai-as-promised: ^7.1.2 chai-http: ^4.3.0 compression: ^1.7.4 concurrently: ^7.0.0 @@ -16263,6 +16265,22 @@ __metadata: languageName: node linkType: hard +"@types/chai-as-promised@npm:^7.1.8": + version: 7.1.8 + resolution: "@types/chai-as-promised@npm:7.1.8" + dependencies: + "@types/chai": "*" + checksum: f0e5eab451b91bc1e289ed89519faf6591932e8a28d2ec9bbe95826eb73d28fe43713633e0c18706f3baa560a7d97e7c7c20dc53ce639e5d75bac46b2a50bf21 + languageName: node + linkType: hard + +"@types/chai@npm:*": + version: 4.3.16 + resolution: "@types/chai@npm:4.3.16" + checksum: bb5f52d1b70534ed8b4bf74bd248add003ffe1156303802ea367331607c06b494da885ffbc2b674a66b4f90c9ee88759790a5f243879f6759f124f22328f5e95 + languageName: node + linkType: hard + "@types/chai@npm:4": version: 4.3.1 resolution: "@types/chai@npm:4.3.1" @@ -22348,6 +22366,17 @@ __metadata: languageName: node linkType: hard +"chai-as-promised@npm:^7.1.2": + version: 7.1.2 + resolution: "chai-as-promised@npm:7.1.2" + dependencies: + check-error: ^1.0.2 + peerDependencies: + chai: ">= 2.1.2 < 6" + checksum: 671ee980054eb23a523875c1d22929a2ac05d89b5428e1fd12800f54fc69baf41014667b87e2368e2355ee2a3140d3e3d7d5a1f8638b07cfefd7fe38a149e3f6 + languageName: node + linkType: hard + "chai-http@npm:^4.3.0": version: 4.3.0 resolution: "chai-http@npm:4.3.0" From 0f4494f6d7ea033381624f7320dfda208ac98ec7 Mon Sep 17 00:00:00 2001 From: Chuck Driesler Date: Thu, 16 May 2024 13:31:10 +0100 Subject: [PATCH 8/9] update tests --- .../server/modules/automate/errors/runs.ts | 2 +- .../automate/repositories/automations.ts | 29 +- .../automate/services/runsManagement.ts | 105 +- .../modules/automate/tests/trigger.spec.ts | 1922 +++++++++-------- 4 files changed, 1035 insertions(+), 1023 deletions(-) diff --git a/packages/server/modules/automate/errors/runs.ts b/packages/server/modules/automate/errors/runs.ts index 7233e2fa6f..1d58740455 100644 --- a/packages/server/modules/automate/errors/runs.ts +++ b/packages/server/modules/automate/errors/runs.ts @@ -5,7 +5,7 @@ export class FunctionRunNotFoundError extends BaseError { static code = 'FUNCTION_RUN_NOT_FOUND' } -export class FunctionRunReportStatusesError extends BaseError { +export class FunctionRunReportStatusError extends BaseError { static defaultMessage = 'An error occurred while updating function run report statuses' static code = 'FUNCTION_RUN_REPORT_STATUSES_ERROR' diff --git a/packages/server/modules/automate/repositories/automations.ts b/packages/server/modules/automate/repositories/automations.ts index 56b5d4c8df..4006ced91d 100644 --- a/packages/server/modules/automate/repositories/automations.ts +++ b/packages/server/modules/automate/repositories/automations.ts @@ -153,6 +153,7 @@ export async function upsertAutomationRun(automationRun: InsertableAutomationRun } export async function getFunctionRun(functionRunId: string) { + console.log({ functionRunId }) const q = AutomationFunctionRuns.knex() .select< Array< @@ -166,7 +167,7 @@ export async function getFunctionRun(functionRunId: string) { AutomationRuns.col.automationRevisionId, AutomationRevisions.col.automationId ]) - .where(AutomationFunctionRuns.col.runId, functionRunId) + .where(AutomationFunctionRuns.col.id, functionRunId) .innerJoin( AutomationRuns.name, AutomationRuns.col.id, @@ -264,11 +265,11 @@ export async function getFullAutomationRunById( return run ? { - ...formatJsonArrayRecords(run.runs)[0], - triggers: formatJsonArrayRecords(run.triggers), - functionRuns: formatJsonArrayRecords(run.functionRuns), - automationId: run.automationId - } + ...formatJsonArrayRecords(run.runs)[0], + triggers: formatJsonArrayRecords(run.triggers), + functionRuns: formatJsonArrayRecords(run.functionRuns), + automationId: run.automationId + } : null } @@ -352,11 +353,11 @@ export async function storeAutomationRevision(revision: InsertableAutomationRevi // Unset 'active in revision' for all other revisions ...(revision.active ? [ - AutomationRevisions.knex() - .where(AutomationRevisions.col.automationId, newRev.automationId) - .andWhereNot(AutomationRevisions.col.id, newRev.id) - .update(AutomationRevisions.withoutTablePrefix.col.active, false) - ] + AutomationRevisions.knex() + .where(AutomationRevisions.col.automationId, newRev.automationId) + .andWhereNot(AutomationRevisions.col.id, newRev.id) + .update(AutomationRevisions.withoutTablePrefix.col.active, false) + ] : []) ]) @@ -837,9 +838,9 @@ export const getAutomationProjects = async (params: { Automations.colAs('id', 'automationId'), ...(userId ? [ - // Getting first role from grouped results - knex.raw(`(array_agg("stream_acl"."role"))[1] as role`) - ] + // Getting first role from grouped results + knex.raw(`(array_agg("stream_acl"."role"))[1] as role`) + ] : []) ]) .whereIn(Automations.col.id, automationIds) diff --git a/packages/server/modules/automate/services/runsManagement.ts b/packages/server/modules/automate/services/runsManagement.ts index 899065d9f1..c1282acbe2 100644 --- a/packages/server/modules/automate/services/runsManagement.ts +++ b/packages/server/modules/automate/services/runsManagement.ts @@ -1,5 +1,7 @@ -import { FunctionRunReportStatusesError } from '@/modules/automate/errors/runs' -import { FunctionNotFoundError } from '@/modules/automate/errors/management' +import { + FunctionRunReportStatusError, + FunctionRunNotFoundError +} from '@/modules/automate/errors/runs' import { AutomateRunsEmitter } from '@/modules/automate/events/runs' import { AutomationFunctionRunRecord, @@ -41,7 +43,7 @@ export const validateStatusChange = ( const newStatusRank = AutomationRunStatusOrder[newStatus] if (newStatusRank <= previousStatusRank) { - throw new FunctionRunReportStatusesError( + throw new FunctionRunReportStatusError( `Invalid status change. Attempting to move from '${previousStatus}' to '${newStatus}'.` ) } @@ -49,13 +51,13 @@ export const validateStatusChange = ( export const validateContextView = (contextView: string) => { if (!contextView.length) { - throw new FunctionRunReportStatusesError( + throw new FunctionRunReportStatusError( 'Context view must be a valid relative URL' ) } if (!contextView.startsWith('/')) { - throw new FunctionRunReportStatusesError( + throw new FunctionRunReportStatusError( 'Context view must start with a forward slash' ) } @@ -64,7 +66,7 @@ export const validateContextView = (contextView: string) => { try { new URL(contextView, 'https://unimportant.com') } catch (e) { - throw new FunctionRunReportStatusesError('Invalid relative URL') + throw new FunctionRunReportStatusError('Invalid relative URL') } } @@ -96,58 +98,59 @@ export type ReportFunctionRunStatusDeps = { export const reportFunctionRunStatus = (deps: ReportFunctionRunStatusDeps) => - async ( - params: Pick< - AutomationFunctionRunRecord, - 'runId' | 'status' | 'statusMessage' | 'contextView' | 'results' - > - ): Promise => { - const { - getAutomationFunctionRunRecord, - upsertAutomationFunctionRunRecord, - automationRunUpdater - } = deps - const { runId, ...statusReportData } = params - - const currentFunctionRunRecord = await getAutomationFunctionRunRecord(runId) - - if (!currentFunctionRunRecord) { - throw new FunctionNotFoundError() - } + async ( + params: Pick< + AutomationFunctionRunRecord, + 'runId' | 'status' | 'statusMessage' | 'contextView' | 'results' + > + ): Promise => { + const { + getAutomationFunctionRunRecord, + upsertAutomationFunctionRunRecord, + automationRunUpdater + } = deps + const { runId, ...statusReportData } = params - if (statusReportData.results) { - console.log(statusReportData.results) - Automate.AutomateTypes.formatResultsSchema(statusReportData.results) - } + const currentFunctionRunRecordResult = await getAutomationFunctionRunRecord(runId) - if (statusReportData.contextView) validateContextView(statusReportData.contextView) + if (!currentFunctionRunRecordResult) { + throw new FunctionRunNotFoundError() + } - const currentStatus = currentFunctionRunRecord.status - const nextStatus = statusReportData.status + const { automationId, ...currentFunctionRunRecord } = currentFunctionRunRecordResult - validateStatusChange(currentStatus, nextStatus) + if (statusReportData.results) { + Automate.AutomateTypes.formatResultsSchema(statusReportData.results) + } - const elapsed = new Date().getTime() - currentFunctionRunRecord.createdAt.getTime() + if (statusReportData.contextView) validateContextView(statusReportData.contextView) - const nextFunctionRunRecord: AutomationFunctionRunRecord = { - ...currentFunctionRunRecord, - ...statusReportData, - elapsed - } + const currentStatus = currentFunctionRunRecord.status + const nextStatus = statusReportData.status - await upsertAutomationFunctionRunRecord(nextFunctionRunRecord) + validateStatusChange(currentStatus, nextStatus) - const updatedRun = await automationRunUpdater({ - id: runId, - status: resolveStatusFromFunctionRunStatuses([nextStatus]), - updatedAt: new Date() - }) + const elapsed = new Date().getTime() - currentFunctionRunRecord.createdAt.getTime() - await AutomateRunsEmitter.emit(AutomateRunsEmitter.events.StatusUpdated, { - run: updatedRun, - functionRuns: [nextFunctionRunRecord], - automationId: currentFunctionRunRecord.automationId - }) + const nextFunctionRunRecord = { + ...currentFunctionRunRecord, + ...statusReportData, + elapsed + } - return true - } + await upsertAutomationFunctionRunRecord(nextFunctionRunRecord) + + const updatedRun = await automationRunUpdater({ + id: runId, + status: resolveStatusFromFunctionRunStatuses([nextStatus]), + updatedAt: new Date() + }) + + await AutomateRunsEmitter.emit(AutomateRunsEmitter.events.StatusUpdated, { + run: updatedRun, + functionRuns: [nextFunctionRunRecord], + automationId + }) + + return true + } diff --git a/packages/server/modules/automate/tests/trigger.spec.ts b/packages/server/modules/automate/tests/trigger.spec.ts index a1970d5253..84e8dc7e9e 100644 --- a/packages/server/modules/automate/tests/trigger.spec.ts +++ b/packages/server/modules/automate/tests/trigger.spec.ts @@ -1,3 +1,7 @@ +import { + FunctionRunReportStatusError, + FunctionRunNotFoundError +} from '@/modules/automate/errors/runs' import { ManuallyTriggerAutomationDeps, ensureRunConditions, @@ -61,909 +65,846 @@ import { } from '@/modules/automate/services/encryption' import { buildDecryptor } from '@/modules/shared/utils/libsodium' import { mapGqlStatusToDbStatus } from '@/modules/automate/utils/automateFunctionRunStatus' +import { Automate } from '@speckle/shared' const { FF_AUTOMATE_MODULE_ENABLED } = Environment.getFeatureFlags() -;(FF_AUTOMATE_MODULE_ENABLED ? describe : describe.skip)( - 'Automate triggers @automate', - () => { - const testUser: BasicTestUser = { - id: cryptoRandomString({ length: 10 }), - name: 'The Automaton', - email: 'the@automaton.com' - } + ; (FF_AUTOMATE_MODULE_ENABLED ? describe : describe.skip)( + 'Automate triggers @automate', + () => { + const testUser: BasicTestUser = { + id: cryptoRandomString({ length: 10 }), + name: 'The Automaton', + email: 'the@automaton.com' + } - const otherUser: BasicTestUser = { - id: cryptoRandomString({ length: 10 }), - name: 'The Automaton Other', - email: 'theother@automaton.com' - } + const otherUser: BasicTestUser = { + id: cryptoRandomString({ length: 10 }), + name: 'The Automaton Other', + email: 'theother@automaton.com' + } - const testUserStream: BasicTestStream = { - id: '', - name: 'First stream', - isPublic: true, - ownerId: '' - } + const testUserStream: BasicTestStream = { + id: '', + name: 'First stream', + isPublic: true, + ownerId: '' + } - const otherUserStream: BasicTestStream = { - id: '', - name: 'Other stream', - isPublic: true, - ownerId: '' - } + const otherUserStream: BasicTestStream = { + id: '', + name: 'Other stream', + isPublic: true, + ownerId: '' + } + + let testUserStreamModel: BranchRecord + let createdAutomation: Awaited>> + let createdRevision: Awaited< + ReturnType> + > + let publicKey: string + + before(async () => { + await beforeEachContext() + await createTestUsers([testUser, otherUser]) + publicKey = await getEncryptionPublicKey() + + const createAutomation = buildAutomationCreate() + const createRevision = buildAutomationRevisionCreate() - let testUserStreamModel: BranchRecord - let createdAutomation: Awaited>> - let createdRevision: Awaited< - ReturnType> - > - let publicKey: string - - before(async () => { - await beforeEachContext() - await createTestUsers([testUser, otherUser]) - publicKey = await getEncryptionPublicKey() - - const createAutomation = buildAutomationCreate() - const createRevision = buildAutomationRevisionCreate() - - await createTestStreams([ - [testUserStream, testUser], - [otherUserStream, otherUser] - ]) - - const [projectModel, newAutomation] = await Promise.all([ - getLatestStreamBranch(testUserStream.id), - createAutomation({ + await createTestStreams([ + [testUserStream, testUser], + [otherUserStream, otherUser] + ]) + + const [projectModel, newAutomation] = await Promise.all([ + getLatestStreamBranch(testUserStream.id), + createAutomation({ + userId: testUser.id, + projectId: testUserStream.id, + input: { + name: 'Manually Triggerable Automation', + enabled: true + } + }) + ]) + testUserStreamModel = projectModel + createdAutomation = newAutomation + + createdRevision = await createRevision({ userId: testUser.id, - projectId: testUserStream.id, input: { - name: 'Manually Triggerable Automation', - enabled: true - } - }) - ]) - testUserStreamModel = projectModel - createdAutomation = newAutomation - - createdRevision = await createRevision({ - userId: testUser.id, - input: { - automationId: createdAutomation.automation.id, - triggerDefinitions: { - version: 1.0, - definitions: [{ type: 'VERSION_CREATED', modelId: testUserStreamModel.id }] + automationId: createdAutomation.automation.id, + triggerDefinitions: { + version: 1.0, + definitions: [{ type: 'VERSION_CREATED', modelId: testUserStreamModel.id }] + }, + functions: [ + { + functionReleaseId: generateFunctionReleaseId(), + functionId: generateFunctionId(), + parameters: null + } + ] }, - functions: [ + projectId: testUserStream.id + }) + + expect(createdRevision).to.be.ok + }) + describe('On model version create', () => { + it('No trigger no run', async () => { + const triggered: Record = {} + await onModelVersionCreate({ + getTriggers: async () => [], + triggerFunction: async ({ manifest, revisionId }) => { + triggered[revisionId] = manifest + return { automationRunId: cryptoRandomString({ length: 10 }) } + } + })({ + modelId: cryptoRandomString({ length: 10 }), + versionId: cryptoRandomString({ length: 10 }), + projectId: cryptoRandomString({ length: 10 }) + }) + expect(Object.keys(triggered)).length(0) + }) + it('Triggers all automation runs associated with the model', async () => { + const storedTriggers: AutomationTriggerDefinitionRecord< + typeof VersionCreationTriggerType + >[] = [ + { + triggerType: VersionCreationTriggerType, + triggeringId: cryptoRandomString({ length: 10 }), + automationRevisionId: cryptoRandomString({ length: 10 }) + }, + { + triggerType: VersionCreationTriggerType, + triggeringId: cryptoRandomString({ length: 10 }), + automationRevisionId: cryptoRandomString({ length: 10 }) + } + ] + const triggered: Record = {} + const versionId = cryptoRandomString({ length: 10 }) + const projectId = cryptoRandomString({ length: 10 }) + + await onModelVersionCreate({ + getTriggers: async < + T extends AutomationTriggerType = AutomationTriggerType + >() => storedTriggers as AutomationTriggerDefinitionRecord[], + triggerFunction: async ({ revisionId, manifest }) => { + if (!isVersionCreatedTriggerManifest(manifest)) { + throw new Error('unexpected trigger type') + } + + triggered[revisionId] = manifest + return { automationRunId: cryptoRandomString({ length: 10 }) } + } + })({ + modelId: cryptoRandomString({ length: 10 }), + versionId, + projectId + }) + expect(Object.keys(triggered)).length(storedTriggers.length) + storedTriggers.forEach((st) => { + const expectedTrigger: VersionCreatedTriggerManifest = { + versionId, + modelId: st.triggeringId, + triggerType: st.triggerType, + projectId + } + expect(triggered[st.automationRevisionId]).deep.equal(expectedTrigger) + }) + }) + it('Failing automation runs do NOT break other runs.', async () => { + const storedTriggers: AutomationTriggerDefinitionRecord[] = [ + { + triggerType: VersionCreationTriggerType, + triggeringId: cryptoRandomString({ length: 10 }), + automationRevisionId: cryptoRandomString({ length: 10 }) + }, { - functionReleaseId: generateFunctionReleaseId(), - functionId: generateFunctionId(), - parameters: null + triggerType: VersionCreationTriggerType, + triggeringId: cryptoRandomString({ length: 10 }), + automationRevisionId: cryptoRandomString({ length: 10 }) } ] - }, - projectId: testUserStream.id - }) + const triggered: Record = {} + const versionId = cryptoRandomString({ length: 10 }) + await onModelVersionCreate({ + getTriggers: async < + T extends AutomationTriggerType = AutomationTriggerType + >() => storedTriggers as AutomationTriggerDefinitionRecord[], + triggerFunction: async ({ revisionId, manifest }) => { + if (!isVersionCreatedTriggerManifest(manifest)) { + throw new Error('unexpected trigger type') + } + if (revisionId === storedTriggers[0].automationRevisionId) + throw new Error('first one is borked') - expect(createdRevision).to.be.ok - }) - describe('On model version create', () => { - it('No trigger no run', async () => { - const triggered: Record = {} - await onModelVersionCreate({ - getTriggers: async () => [], - triggerFunction: async ({ manifest, revisionId }) => { - triggered[revisionId] = manifest - return { automationRunId: cryptoRandomString({ length: 10 }) } - } - })({ - modelId: cryptoRandomString({ length: 10 }), - versionId: cryptoRandomString({ length: 10 }), - projectId: cryptoRandomString({ length: 10 }) + triggered[revisionId] = manifest + return { automationRunId: cryptoRandomString({ length: 10 }) } + } + })({ + modelId: cryptoRandomString({ length: 10 }), + versionId, + projectId: cryptoRandomString({ length: 10 }) + }) + expect(Object.keys(triggered)).length(storedTriggers.length - 1) }) - expect(Object.keys(triggered)).length(0) }) - it('Triggers all automation runs associated with the model', async () => { - const storedTriggers: AutomationTriggerDefinitionRecord< - typeof VersionCreationTriggerType - >[] = [ - { - triggerType: VersionCreationTriggerType, - triggeringId: cryptoRandomString({ length: 10 }), - automationRevisionId: cryptoRandomString({ length: 10 }) - }, - { - triggerType: VersionCreationTriggerType, - triggeringId: cryptoRandomString({ length: 10 }), - automationRevisionId: cryptoRandomString({ length: 10 }) + describe('Triggering an automation revision run', () => { + it('Throws if run conditions are not met', async () => { + try { + await triggerAutomationRevisionRun({ + automateRunTrigger: async () => ({ + automationRunId: cryptoRandomString({ length: 10 }) + }), + getFunctionInputDecryptor: getFunctionInputDecryptor({ buildDecryptor }), + getEncryptionKeyPairFor + })({ + revisionId: cryptoRandomString({ length: 10 }), + manifest: { + versionId: cryptoRandomString({ length: 10 }), + triggerType: VersionCreationTriggerType, + modelId: cryptoRandomString({ length: 10 }) + } + }) + throw 'this should have thrown' + } catch (error) { + if (!(error instanceof Error)) throw error + expect(error.message).contains( + "Cannot trigger the given revision, it doesn't exist" + ) + } + }) + it('Saves run with an error if automate run trigger fails', async () => { + const userId = testUser.id + + const project = { + name: cryptoRandomString({ length: 10 }), + id: cryptoRandomString({ length: 10 }), + ownerId: userId, + isPublic: true } - ] - const triggered: Record = {} - const versionId = cryptoRandomString({ length: 10 }) - const projectId = cryptoRandomString({ length: 10 }) - - await onModelVersionCreate({ - getTriggers: async < - T extends AutomationTriggerType = AutomationTriggerType - >() => storedTriggers as AutomationTriggerDefinitionRecord[], - triggerFunction: async ({ revisionId, manifest }) => { - if (!isVersionCreatedTriggerManifest(manifest)) { - throw new Error('unexpected trigger type') - } - triggered[revisionId] = manifest - return { automationRunId: cryptoRandomString({ length: 10 }) } + await createTestStream(project, testUser) + const version = { + id: cryptoRandomString({ length: 10 }), + streamId: project.id, + objectId: null, + authorId: userId } - })({ - modelId: cryptoRandomString({ length: 10 }), - versionId, - projectId - }) - expect(Object.keys(triggered)).length(storedTriggers.length) - storedTriggers.forEach((st) => { - const expectedTrigger: VersionCreatedTriggerManifest = { - versionId, - modelId: st.triggeringId, - triggerType: st.triggerType, - projectId + // @ts-expect-error force setting the objectId to null + await createTestCommit(version) + // create automation, + const automation = { + id: cryptoRandomString({ length: 10 }), + createdAt: new Date(), + updatedAt: new Date(), + name: cryptoRandomString({ length: 15 }), + enabled: true, + projectId: project.id, + executionEngineAutomationId: cryptoRandomString({ length: 10 }), + userId } - expect(triggered[st.automationRevisionId]).deep.equal(expectedTrigger) - }) - }) - it('Failing automation runs do NOT break other runs.', async () => { - const storedTriggers: AutomationTriggerDefinitionRecord[] = [ - { - triggerType: VersionCreationTriggerType, - triggeringId: cryptoRandomString({ length: 10 }), - automationRevisionId: cryptoRandomString({ length: 10 }) - }, - { - triggerType: VersionCreationTriggerType, - triggeringId: cryptoRandomString({ length: 10 }), - automationRevisionId: cryptoRandomString({ length: 10 }) + const automationToken = { + automationId: automation.id, + automateToken: cryptoRandomString({ length: 10 }), + automateRefreshToken: cryptoRandomString({ length: 10 }) } - ] - const triggered: Record = {} - const versionId = cryptoRandomString({ length: 10 }) - await onModelVersionCreate({ - getTriggers: async < - T extends AutomationTriggerType = AutomationTriggerType - >() => storedTriggers as AutomationTriggerDefinitionRecord[], - triggerFunction: async ({ revisionId, manifest }) => { - if (!isVersionCreatedTriggerManifest(manifest)) { - throw new Error('unexpected trigger type') - } - if (revisionId === storedTriggers[0].automationRevisionId) - throw new Error('first one is borked') + await storeAutomation(automation, automationToken) - triggered[revisionId] = manifest - return { automationRunId: cryptoRandomString({ length: 10 }) } + const automationRevisionId = cryptoRandomString({ length: 10 }) + const trigger = { + triggerType: VersionCreationTriggerType, + triggeringId: cryptoRandomString({ length: 10 }) } - })({ - modelId: cryptoRandomString({ length: 10 }), - versionId, - projectId: cryptoRandomString({ length: 10 }) - }) - expect(Object.keys(triggered)).length(storedTriggers.length - 1) - }) - }) - describe('Triggering an automation revision run', () => { - it('Throws if run conditions are not met', async () => { - try { - await triggerAutomationRevisionRun({ - automateRunTrigger: async () => ({ - automationRunId: cryptoRandomString({ length: 10 }) - }), + // create revision, + await storeAutomationRevision({ + id: automationRevisionId, + createdAt: new Date(), + automationId: automation.id, + active: true, + triggers: [trigger], + userId, + publicKey, + functions: [ + { + functionInputs: null, + functionReleaseId: cryptoRandomString({ length: 10 }), + functionId: cryptoRandomString({ length: 10 }) + } + ] + }) + const thrownError = 'trigger failed' + const { automationRunId } = await triggerAutomationRevisionRun({ + automateRunTrigger: async () => { + throw new Error(thrownError) + }, getFunctionInputDecryptor: getFunctionInputDecryptor({ buildDecryptor }), getEncryptionKeyPairFor })({ - revisionId: cryptoRandomString({ length: 10 }), + revisionId: automationRevisionId, manifest: { - versionId: cryptoRandomString({ length: 10 }), - triggerType: VersionCreationTriggerType, - modelId: cryptoRandomString({ length: 10 }) + versionId: version.id, + modelId: trigger.triggeringId, + triggerType: trigger.triggerType } }) - throw 'this should have thrown' - } catch (error) { - if (!(error instanceof Error)) throw error - expect(error.message).contains( - "Cannot trigger the given revision, it doesn't exist" - ) - } - }) - it('Saves run with an error if automate run trigger fails', async () => { - const userId = testUser.id - - const project = { - name: cryptoRandomString({ length: 10 }), - id: cryptoRandomString({ length: 10 }), - ownerId: userId, - isPublic: true - } - await createTestStream(project, testUser) - const version = { - id: cryptoRandomString({ length: 10 }), - streamId: project.id, - objectId: null, - authorId: userId - } - // @ts-expect-error force setting the objectId to null - await createTestCommit(version) - // create automation, - const automation = { - id: cryptoRandomString({ length: 10 }), - createdAt: new Date(), - updatedAt: new Date(), - name: cryptoRandomString({ length: 15 }), - enabled: true, - projectId: project.id, - executionEngineAutomationId: cryptoRandomString({ length: 10 }), - userId - } - const automationToken = { - automationId: automation.id, - automateToken: cryptoRandomString({ length: 10 }), - automateRefreshToken: cryptoRandomString({ length: 10 }) - } - await storeAutomation(automation, automationToken) + const storedRun = await getFullAutomationRunById(automationRunId) + if (!storedRun) throw 'cant fint the stored run' - const automationRevisionId = cryptoRandomString({ length: 10 }) - const trigger = { - triggerType: VersionCreationTriggerType, - triggeringId: cryptoRandomString({ length: 10 }) - } - // create revision, - await storeAutomationRevision({ - id: automationRevisionId, - createdAt: new Date(), - automationId: automation.id, - active: true, - triggers: [trigger], - userId, - publicKey, - functions: [ - { - functionInputs: null, - functionReleaseId: cryptoRandomString({ length: 10 }), - functionId: cryptoRandomString({ length: 10 }) - } - ] - }) - const thrownError = 'trigger failed' - const { automationRunId } = await triggerAutomationRevisionRun({ - automateRunTrigger: async () => { - throw new Error(thrownError) - }, - getFunctionInputDecryptor: getFunctionInputDecryptor({ buildDecryptor }), - getEncryptionKeyPairFor - })({ - revisionId: automationRevisionId, - manifest: { - versionId: version.id, - modelId: trigger.triggeringId, - triggerType: trigger.triggerType + const expectedStatus = 'error' + + expect(storedRun.status).to.equal(expectedStatus) + for (const run of storedRun.functionRuns) { + expect(run.status).to.equal(expectedStatus) + expect(run.statusMessage).to.equal(thrownError) } }) + it('Saves run with the execution engine run id if trigger is successful', async () => { + // create user, project, model, version - const storedRun = await getFullAutomationRunById(automationRunId) - if (!storedRun) throw 'cant fint the stored run' - - const expectedStatus = 'error' - - expect(storedRun.status).to.equal(expectedStatus) - for (const run of storedRun.functionRuns) { - expect(run.status).to.equal(expectedStatus) - expect(run.statusMessage).to.equal(thrownError) - } - }) - it('Saves run with the execution engine run id if trigger is successful', async () => { - // create user, project, model, version - - const userId = testUser.id - - const project = { - name: cryptoRandomString({ length: 10 }), - id: cryptoRandomString({ length: 10 }), - ownerId: userId, - isPublic: true - } + const userId = testUser.id - await createTestStream(project, testUser) - const version = { - id: cryptoRandomString({ length: 10 }), - streamId: project.id, - objectId: null, - authorId: userId - } - // @ts-expect-error force setting the objectId to null - await createTestCommit(version) - // create automation, - const automation = { - id: cryptoRandomString({ length: 10 }), - createdAt: new Date(), - updatedAt: new Date(), - name: cryptoRandomString({ length: 15 }), - enabled: true, - projectId: project.id, - executionEngineAutomationId: cryptoRandomString({ length: 10 }), - userId - } - const automationToken = { - automationId: automation.id, - automateToken: cryptoRandomString({ length: 10 }), - automateRefreshToken: cryptoRandomString({ length: 10 }) - } - await storeAutomation(automation, automationToken) - - const automationRevisionId = cryptoRandomString({ length: 10 }) - const trigger = { - triggerType: VersionCreationTriggerType, - triggeringId: cryptoRandomString({ length: 10 }) - } - // create revision, - await storeAutomationRevision({ - id: automationRevisionId, - createdAt: new Date(), - automationId: automation.id, - active: true, - triggers: [trigger], - userId, - publicKey, - functions: [ - { - functionInputs: null, - functionReleaseId: cryptoRandomString({ length: 10 }), - functionId: cryptoRandomString({ length: 10 }) - } - ] - }) - const executionEngineRunId = cryptoRandomString({ length: 10 }) - const { automationRunId } = await triggerAutomationRevisionRun({ - automateRunTrigger: async () => ({ - automationRunId: executionEngineRunId - }), - getFunctionInputDecryptor: getFunctionInputDecryptor({ buildDecryptor }), - getEncryptionKeyPairFor - })({ - revisionId: automationRevisionId, - manifest: { - versionId: version.id, - modelId: trigger.triggeringId, - triggerType: trigger.triggerType + const project = { + name: cryptoRandomString({ length: 10 }), + id: cryptoRandomString({ length: 10 }), + ownerId: userId, + isPublic: true } - }) - const storedRun = await getFullAutomationRunById(automationRunId) - if (!storedRun) throw 'cant fint the stored run' - - const expectedStatus = 'pending' + await createTestStream(project, testUser) + const version = { + id: cryptoRandomString({ length: 10 }), + streamId: project.id, + objectId: null, + authorId: userId + } + // @ts-expect-error force setting the objectId to null + await createTestCommit(version) + // create automation, + const automation = { + id: cryptoRandomString({ length: 10 }), + createdAt: new Date(), + updatedAt: new Date(), + name: cryptoRandomString({ length: 15 }), + enabled: true, + projectId: project.id, + executionEngineAutomationId: cryptoRandomString({ length: 10 }), + userId + } + const automationToken = { + automationId: automation.id, + automateToken: cryptoRandomString({ length: 10 }), + automateRefreshToken: cryptoRandomString({ length: 10 }) + } + await storeAutomation(automation, automationToken) - expect(storedRun.status).to.equal(expectedStatus) - expect(storedRun.executionEngineRunId).to.equal(executionEngineRunId) - for (const run of storedRun.functionRuns) { - expect(run.status).to.equal(expectedStatus) - } - }) - }) - describe('Run conditions are NOT met if', () => { - it("the referenced revision doesn't exist", async () => { - try { - await ensureRunConditions({ - revisionGetter: async () => null, - versionGetter: async () => undefined, - automationTokenGetter: async () => null + const automationRevisionId = cryptoRandomString({ length: 10 }) + const trigger = { + triggerType: VersionCreationTriggerType, + triggeringId: cryptoRandomString({ length: 10 }) + } + // create revision, + await storeAutomationRevision({ + id: automationRevisionId, + createdAt: new Date(), + automationId: automation.id, + active: true, + triggers: [trigger], + userId, + publicKey, + functions: [ + { + functionInputs: null, + functionReleaseId: cryptoRandomString({ length: 10 }), + functionId: cryptoRandomString({ length: 10 }) + } + ] + }) + const executionEngineRunId = cryptoRandomString({ length: 10 }) + const { automationRunId } = await triggerAutomationRevisionRun({ + automateRunTrigger: async () => ({ + automationRunId: executionEngineRunId + }), + getFunctionInputDecryptor: getFunctionInputDecryptor({ buildDecryptor }), + getEncryptionKeyPairFor })({ - revisionId: cryptoRandomString({ length: 10 }), + revisionId: automationRevisionId, manifest: { - triggerType: VersionCreationTriggerType, - modelId: cryptoRandomString({ length: 10 }), - versionId: cryptoRandomString({ length: 10 }) + versionId: version.id, + modelId: trigger.triggeringId, + triggerType: trigger.triggerType } }) - throw 'this should have thrown' - } catch (error) { - if (!(error instanceof Error)) throw error - expect(error.message).contains( - "Cannot trigger the given revision, it doesn't exist" - ) - } + + const storedRun = await getFullAutomationRunById(automationRunId) + if (!storedRun) throw 'cant fint the stored run' + + const expectedStatus = 'pending' + + expect(storedRun.status).to.equal(expectedStatus) + expect(storedRun.executionEngineRunId).to.equal(executionEngineRunId) + for (const run of storedRun.functionRuns) { + expect(run.status).to.equal(expectedStatus) + } + }) }) - it('the automation is not enabled', async () => { - try { - await ensureRunConditions({ - revisionGetter: async () => ({ - id: cryptoRandomString({ length: 10 }), - name: cryptoRandomString({ length: 10 }), - projectId: cryptoRandomString({ length: 10 }), - enabled: false, - createdAt: new Date(), - updatedAt: new Date(), - executionEngineAutomationId: cryptoRandomString({ length: 10 }), - userId: cryptoRandomString({ length: 10 }), - revision: { + describe('Run conditions are NOT met if', () => { + it("the referenced revision doesn't exist", async () => { + try { + await ensureRunConditions({ + revisionGetter: async () => null, + versionGetter: async () => undefined, + automationTokenGetter: async () => null + })({ + revisionId: cryptoRandomString({ length: 10 }), + manifest: { + triggerType: VersionCreationTriggerType, + modelId: cryptoRandomString({ length: 10 }), + versionId: cryptoRandomString({ length: 10 }) + } + }) + throw 'this should have thrown' + } catch (error) { + if (!(error instanceof Error)) throw error + expect(error.message).contains( + "Cannot trigger the given revision, it doesn't exist" + ) + } + }) + it('the automation is not enabled', async () => { + try { + await ensureRunConditions({ + revisionGetter: async () => ({ id: cryptoRandomString({ length: 10 }), + name: cryptoRandomString({ length: 10 }), + projectId: cryptoRandomString({ length: 10 }), + enabled: false, createdAt: new Date(), - publicKey, + updatedAt: new Date(), + executionEngineAutomationId: cryptoRandomString({ length: 10 }), userId: cryptoRandomString({ length: 10 }), - active: false, - triggers: [], - functions: [], - automationId: cryptoRandomString({ length: 10 }), - automationToken: cryptoRandomString({ length: 15 }) + revision: { + id: cryptoRandomString({ length: 10 }), + createdAt: new Date(), + publicKey, + userId: cryptoRandomString({ length: 10 }), + active: false, + triggers: [], + functions: [], + automationId: cryptoRandomString({ length: 10 }), + automationToken: cryptoRandomString({ length: 15 }) + } + }), + versionGetter: async () => undefined, + automationTokenGetter: async () => null + })({ + revisionId: cryptoRandomString({ length: 10 }), + manifest: { + triggerType: VersionCreationTriggerType, + modelId: cryptoRandomString({ length: 10 }), + versionId: cryptoRandomString({ length: 10 }) } - }), - versionGetter: async () => undefined, - automationTokenGetter: async () => null - })({ - revisionId: cryptoRandomString({ length: 10 }), - manifest: { - triggerType: VersionCreationTriggerType, - modelId: cryptoRandomString({ length: 10 }), - versionId: cryptoRandomString({ length: 10 }) - } - }) - throw 'this should have thrown' - } catch (error) { - if (!(error instanceof Error)) throw error - expect(error.message).contains( - 'The automation is not enabled, cannot trigger it' - ) - } - }) - it('the revision is not active', async () => { - try { - await ensureRunConditions({ - revisionGetter: async () => ({ - id: cryptoRandomString({ length: 10 }), - name: cryptoRandomString({ length: 10 }), - projectId: cryptoRandomString({ length: 10 }), - enabled: true, - createdAt: new Date(), - updatedAt: new Date(), - executionEngineAutomationId: cryptoRandomString({ length: 10 }), - userId: cryptoRandomString({ length: 10 }), - revision: { - publicKey, - active: false, - triggers: [], - functions: [], - automationId: cryptoRandomString({ length: 10 }), - automationToken: cryptoRandomString({ length: 15 }), + }) + throw 'this should have thrown' + } catch (error) { + if (!(error instanceof Error)) throw error + expect(error.message).contains( + 'The automation is not enabled, cannot trigger it' + ) + } + }) + it('the revision is not active', async () => { + try { + await ensureRunConditions({ + revisionGetter: async () => ({ id: cryptoRandomString({ length: 10 }), + name: cryptoRandomString({ length: 10 }), + projectId: cryptoRandomString({ length: 10 }), + enabled: true, createdAt: new Date(), - userId: cryptoRandomString({ length: 10 }) + updatedAt: new Date(), + executionEngineAutomationId: cryptoRandomString({ length: 10 }), + userId: cryptoRandomString({ length: 10 }), + revision: { + publicKey, + active: false, + triggers: [], + functions: [], + automationId: cryptoRandomString({ length: 10 }), + automationToken: cryptoRandomString({ length: 15 }), + id: cryptoRandomString({ length: 10 }), + createdAt: new Date(), + userId: cryptoRandomString({ length: 10 }) + } + }), + versionGetter: async () => undefined, + automationTokenGetter: async () => null + })({ + revisionId: cryptoRandomString({ length: 10 }), + manifest: { + triggerType: VersionCreationTriggerType, + modelId: cryptoRandomString({ length: 10 }), + versionId: cryptoRandomString({ length: 10 }) } - }), - versionGetter: async () => undefined, - automationTokenGetter: async () => null - })({ - revisionId: cryptoRandomString({ length: 10 }), - manifest: { - triggerType: VersionCreationTriggerType, - modelId: cryptoRandomString({ length: 10 }), - versionId: cryptoRandomString({ length: 10 }) - } - }) - throw 'this should have thrown' - } catch (error) { - if (!(error instanceof Error)) throw error - expect(error.message).contains( - 'The automation revision is not active, cannot trigger it' - ) - } - }) - it("the revision doesn't have the referenced trigger", async () => { - try { - await ensureRunConditions({ - revisionGetter: async () => ({ - id: cryptoRandomString({ length: 10 }), - createdAt: new Date(), - updatedAt: new Date(), - userId: cryptoRandomString({ length: 10 }), - name: cryptoRandomString({ length: 10 }), - projectId: cryptoRandomString({ length: 10 }), - enabled: true, - executionEngineAutomationId: cryptoRandomString({ length: 10 }), - revision: { - publicKey, + }) + throw 'this should have thrown' + } catch (error) { + if (!(error instanceof Error)) throw error + expect(error.message).contains( + 'The automation revision is not active, cannot trigger it' + ) + } + }) + it("the revision doesn't have the referenced trigger", async () => { + try { + await ensureRunConditions({ + revisionGetter: async () => ({ id: cryptoRandomString({ length: 10 }), createdAt: new Date(), + updatedAt: new Date(), userId: cryptoRandomString({ length: 10 }), - active: true, - triggers: [], - functions: [], - automationId: cryptoRandomString({ length: 10 }), - automationToken: cryptoRandomString({ length: 15 }) + name: cryptoRandomString({ length: 10 }), + projectId: cryptoRandomString({ length: 10 }), + enabled: true, + executionEngineAutomationId: cryptoRandomString({ length: 10 }), + revision: { + publicKey, + id: cryptoRandomString({ length: 10 }), + createdAt: new Date(), + userId: cryptoRandomString({ length: 10 }), + active: true, + triggers: [], + functions: [], + automationId: cryptoRandomString({ length: 10 }), + automationToken: cryptoRandomString({ length: 15 }) + } + }), + versionGetter: async () => undefined, + automationTokenGetter: async () => null + })({ + revisionId: cryptoRandomString({ length: 10 }), + manifest: { + triggerType: VersionCreationTriggerType, + modelId: cryptoRandomString({ length: 10 }), + versionId: cryptoRandomString({ length: 10 }) } - }), - versionGetter: async () => undefined, - automationTokenGetter: async () => null - })({ - revisionId: cryptoRandomString({ length: 10 }), - manifest: { - triggerType: VersionCreationTriggerType, - modelId: cryptoRandomString({ length: 10 }), - versionId: cryptoRandomString({ length: 10 }) - } - }) - throw 'this should have thrown' - } catch (error) { - if (!(error instanceof Error)) throw error - expect(error.message).contains( - "The given revision doesn't have a trigger registered matching the input trigger" - ) - } - }) - it('the trigger is not a versionCreation type', async () => { - const manifest: VersionCreatedTriggerManifest = { - // @ts-expect-error: intentionally using invalid type here - triggerType: 'bogusTrigger' as const, - modelId: cryptoRandomString({ length: 10 }), - versionId: cryptoRandomString({ length: 10 }) - } + }) + throw 'this should have thrown' + } catch (error) { + if (!(error instanceof Error)) throw error + expect(error.message).contains( + "The given revision doesn't have a trigger registered matching the input trigger" + ) + } + }) + it('the trigger is not a versionCreation type', async () => { + const manifest: VersionCreatedTriggerManifest = { + // @ts-expect-error: intentionally using invalid type here + triggerType: 'bogusTrigger' as const, + modelId: cryptoRandomString({ length: 10 }), + versionId: cryptoRandomString({ length: 10 }) + } - try { - await ensureRunConditions({ - revisionGetter: async () => ({ - id: cryptoRandomString({ length: 10 }), - name: cryptoRandomString({ length: 10 }), - projectId: cryptoRandomString({ length: 10 }), - enabled: true, - createdAt: new Date(), - updatedAt: new Date(), - executionEngineAutomationId: cryptoRandomString({ length: 10 }), - userId: cryptoRandomString({ length: 10 }), - revision: { - publicKey, + try { + await ensureRunConditions({ + revisionGetter: async () => ({ id: cryptoRandomString({ length: 10 }), + name: cryptoRandomString({ length: 10 }), + projectId: cryptoRandomString({ length: 10 }), + enabled: true, createdAt: new Date(), + updatedAt: new Date(), + executionEngineAutomationId: cryptoRandomString({ length: 10 }), userId: cryptoRandomString({ length: 10 }), - active: true, - triggers: [ - { - triggeringId: manifest.modelId, - triggerType: manifest.triggerType, - automationRevisionId: cryptoRandomString({ length: 10 }) - } - ], - functions: [], - automationId: cryptoRandomString({ length: 10 }) - } - }), - versionGetter: async () => undefined, - automationTokenGetter: async () => null - })({ - revisionId: cryptoRandomString({ length: 10 }), - manifest - }) - throw 'this should have thrown' - } catch (error) { - if (!(error instanceof Error)) throw error - expect(error.message).contains('Only model version triggers are supported') - } - }) - it("the version that is referenced on the trigger, doesn't exist", async () => { - const manifest: VersionCreatedTriggerManifest = { - triggerType: VersionCreationTriggerType, - modelId: cryptoRandomString({ length: 10 }), - versionId: cryptoRandomString({ length: 10 }), - projectId: cryptoRandomString({ length: 10 }) - } + revision: { + publicKey, + id: cryptoRandomString({ length: 10 }), + createdAt: new Date(), + userId: cryptoRandomString({ length: 10 }), + active: true, + triggers: [ + { + triggeringId: manifest.modelId, + triggerType: manifest.triggerType, + automationRevisionId: cryptoRandomString({ length: 10 }) + } + ], + functions: [], + automationId: cryptoRandomString({ length: 10 }) + } + }), + versionGetter: async () => undefined, + automationTokenGetter: async () => null + })({ + revisionId: cryptoRandomString({ length: 10 }), + manifest + }) + throw 'this should have thrown' + } catch (error) { + if (!(error instanceof Error)) throw error + expect(error.message).contains('Only model version triggers are supported') + } + }) + it("the version that is referenced on the trigger, doesn't exist", async () => { + const manifest: VersionCreatedTriggerManifest = { + triggerType: VersionCreationTriggerType, + modelId: cryptoRandomString({ length: 10 }), + versionId: cryptoRandomString({ length: 10 }), + projectId: cryptoRandomString({ length: 10 }) + } - try { - await ensureRunConditions({ - revisionGetter: async () => ({ - id: cryptoRandomString({ length: 10 }), - name: cryptoRandomString({ length: 10 }), - projectId: cryptoRandomString({ length: 10 }), - enabled: true, - createdAt: new Date(), - updatedAt: new Date(), - executionEngineAutomationId: cryptoRandomString({ length: 10 }), - userId: cryptoRandomString({ length: 10 }), - revision: { + try { + await ensureRunConditions({ + revisionGetter: async () => ({ id: cryptoRandomString({ length: 10 }), + name: cryptoRandomString({ length: 10 }), + projectId: cryptoRandomString({ length: 10 }), + enabled: true, createdAt: new Date(), + updatedAt: new Date(), + executionEngineAutomationId: cryptoRandomString({ length: 10 }), userId: cryptoRandomString({ length: 10 }), - active: true, - publicKey, - triggers: [ - { - triggerType: manifest.triggerType, - triggeringId: manifest.modelId, - automationRevisionId: cryptoRandomString({ length: 10 }) - } - ], - functions: [], - automationId: cryptoRandomString({ length: 10 }), - automationToken: cryptoRandomString({ length: 15 }) - } - }), - versionGetter: async () => undefined, - automationTokenGetter: async () => null - })({ - revisionId: cryptoRandomString({ length: 10 }), - manifest - }) - throw 'this should have thrown' - } catch (error) { - if (!(error instanceof Error)) throw error - expect(error.message).contains('The triggering version is not found') - } - }) - it("the author, that created the triggering version doesn't exist", async () => { - const manifest: VersionCreatedTriggerManifest = { - triggerType: VersionCreationTriggerType, - modelId: cryptoRandomString({ length: 10 }), - versionId: cryptoRandomString({ length: 10 }), - projectId: cryptoRandomString({ length: 10 }) - } + revision: { + id: cryptoRandomString({ length: 10 }), + createdAt: new Date(), + userId: cryptoRandomString({ length: 10 }), + active: true, + publicKey, + triggers: [ + { + triggerType: manifest.triggerType, + triggeringId: manifest.modelId, + automationRevisionId: cryptoRandomString({ length: 10 }) + } + ], + functions: [], + automationId: cryptoRandomString({ length: 10 }), + automationToken: cryptoRandomString({ length: 15 }) + } + }), + versionGetter: async () => undefined, + automationTokenGetter: async () => null + })({ + revisionId: cryptoRandomString({ length: 10 }), + manifest + }) + throw 'this should have thrown' + } catch (error) { + if (!(error instanceof Error)) throw error + expect(error.message).contains('The triggering version is not found') + } + }) + it("the author, that created the triggering version doesn't exist", async () => { + const manifest: VersionCreatedTriggerManifest = { + triggerType: VersionCreationTriggerType, + modelId: cryptoRandomString({ length: 10 }), + versionId: cryptoRandomString({ length: 10 }), + projectId: cryptoRandomString({ length: 10 }) + } - try { - await ensureRunConditions({ - revisionGetter: async () => ({ - id: cryptoRandomString({ length: 10 }), - name: cryptoRandomString({ length: 10 }), - projectId: cryptoRandomString({ length: 10 }), - createdAt: new Date(), - updatedAt: new Date(), - enabled: true, - executionEngineAutomationId: cryptoRandomString({ length: 10 }), - userId: cryptoRandomString({ length: 10 }), - revision: { + try { + await ensureRunConditions({ + revisionGetter: async () => ({ id: cryptoRandomString({ length: 10 }), + name: cryptoRandomString({ length: 10 }), + projectId: cryptoRandomString({ length: 10 }), + createdAt: new Date(), + updatedAt: new Date(), + enabled: true, + executionEngineAutomationId: cryptoRandomString({ length: 10 }), userId: cryptoRandomString({ length: 10 }), - active: true, - publicKey, - triggers: [ - { - triggeringId: manifest.modelId, - triggerType: manifest.triggerType, - automationRevisionId: cryptoRandomString({ length: 10 }) - } - ], + revision: { + id: cryptoRandomString({ length: 10 }), + userId: cryptoRandomString({ length: 10 }), + active: true, + publicKey, + triggers: [ + { + triggeringId: manifest.modelId, + triggerType: manifest.triggerType, + automationRevisionId: cryptoRandomString({ length: 10 }) + } + ], + createdAt: new Date(), + functions: [], + automationId: cryptoRandomString({ length: 10 }), + automationToken: cryptoRandomString({ length: 15 }) + } + }), + versionGetter: async () => ({ + author: null, + id: cryptoRandomString({ length: 10 }), createdAt: new Date(), - functions: [], - automationId: cryptoRandomString({ length: 10 }), - automationToken: cryptoRandomString({ length: 15 }) - } - }), - versionGetter: async () => ({ - author: null, - id: cryptoRandomString({ length: 10 }), - createdAt: new Date(), - message: 'foobar', - parents: [], - referencedObject: cryptoRandomString({ length: 10 }), - totalChildrenCount: null, - sourceApplication: 'test suite', - streamId: cryptoRandomString({ length: 10 }), - branchId: cryptoRandomString({ length: 10 }), - branchName: cryptoRandomString({ length: 10 }) - }), - automationTokenGetter: async () => null - })({ - revisionId: cryptoRandomString({ length: 10 }), - manifest - }) - throw 'this should have thrown' - } catch (error) { - if (!(error instanceof Error)) throw error - expect(error.message).contains( - "The user, that created the triggering version doesn't exist any more" - ) - } - }) - it("the automation doesn't have a token available", async () => { - const manifest: VersionCreatedTriggerManifest = { - triggerType: VersionCreationTriggerType, - modelId: cryptoRandomString({ length: 10 }), - versionId: cryptoRandomString({ length: 10 }), - projectId: cryptoRandomString({ length: 10 }) - } - try { - await ensureRunConditions({ - revisionGetter: async () => ({ - id: cryptoRandomString({ length: 10 }), - name: cryptoRandomString({ length: 10 }), - projectId: cryptoRandomString({ length: 10 }), - enabled: true, - createdAt: new Date(), - updatedAt: new Date(), - executionEngineAutomationId: cryptoRandomString({ length: 10 }), - userId: cryptoRandomString({ length: 10 }), - revision: { + message: 'foobar', + parents: [], + referencedObject: cryptoRandomString({ length: 10 }), + totalChildrenCount: null, + sourceApplication: 'test suite', + streamId: cryptoRandomString({ length: 10 }), + branchId: cryptoRandomString({ length: 10 }), + branchName: cryptoRandomString({ length: 10 }) + }), + automationTokenGetter: async () => null + })({ + revisionId: cryptoRandomString({ length: 10 }), + manifest + }) + throw 'this should have thrown' + } catch (error) { + if (!(error instanceof Error)) throw error + expect(error.message).contains( + "The user, that created the triggering version doesn't exist any more" + ) + } + }) + it("the automation doesn't have a token available", async () => { + const manifest: VersionCreatedTriggerManifest = { + triggerType: VersionCreationTriggerType, + modelId: cryptoRandomString({ length: 10 }), + versionId: cryptoRandomString({ length: 10 }), + projectId: cryptoRandomString({ length: 10 }) + } + try { + await ensureRunConditions({ + revisionGetter: async () => ({ id: cryptoRandomString({ length: 10 }), + name: cryptoRandomString({ length: 10 }), + projectId: cryptoRandomString({ length: 10 }), + enabled: true, + createdAt: new Date(), + updatedAt: new Date(), + executionEngineAutomationId: cryptoRandomString({ length: 10 }), userId: cryptoRandomString({ length: 10 }), + revision: { + id: cryptoRandomString({ length: 10 }), + userId: cryptoRandomString({ length: 10 }), + createdAt: new Date(), + active: true, + publicKey, + triggers: [ + { + triggeringId: manifest.modelId, + triggerType: manifest.triggerType, + automationRevisionId: cryptoRandomString({ length: 10 }) + } + ], + functions: [], + automationId: cryptoRandomString({ length: 10 }), + automationToken: cryptoRandomString({ length: 15 }) + } + }), + versionGetter: async () => ({ + author: cryptoRandomString({ length: 10 }), + id: cryptoRandomString({ length: 10 }), createdAt: new Date(), - active: true, - publicKey, - triggers: [ - { - triggeringId: manifest.modelId, - triggerType: manifest.triggerType, - automationRevisionId: cryptoRandomString({ length: 10 }) - } - ], - functions: [], - automationId: cryptoRandomString({ length: 10 }), - automationToken: cryptoRandomString({ length: 15 }) - } - }), - versionGetter: async () => ({ - author: cryptoRandomString({ length: 10 }), - id: cryptoRandomString({ length: 10 }), - createdAt: new Date(), - message: 'foobar', - parents: [], - referencedObject: cryptoRandomString({ length: 10 }), - totalChildrenCount: null, - sourceApplication: 'test suite', - streamId: cryptoRandomString({ length: 10 }), - branchId: cryptoRandomString({ length: 10 }), - branchName: cryptoRandomString({ length: 10 }) - }), - automationTokenGetter: async () => null - })({ - revisionId: cryptoRandomString({ length: 10 }), - manifest - }) - throw 'this should have thrown' - } catch (error) { - if (!(error instanceof Error)) throw error - expect(error.message).contains('Cannot find a token for the automation') - } - }) - }) - - describe('Run triggered manually', () => { - const buildManuallyTriggerAutomation = ( - overrides?: Partial - ) => { - const trigger = manuallyTriggerAutomation({ - getAutomationTriggerDefinitions, - getAutomation, - getBranchLatestCommits, - triggerFunction: triggerAutomationRevisionRun({ - automateRunTrigger: async () => ({ - automationRunId: cryptoRandomString({ length: 10 }) - }), - getFunctionInputDecryptor: getFunctionInputDecryptor({ buildDecryptor }), - getEncryptionKeyPairFor - }), - ...(overrides || {}) - }) - return trigger - } - - it('fails if referring to nonexistent automation', async () => { - const trigger = buildManuallyTriggerAutomation() - - const e = await expectToThrow( - async () => - await trigger({ - automationId: cryptoRandomString({ length: 10 }), - userId: testUser.id, - projectId: testUserStream.id + message: 'foobar', + parents: [], + referencedObject: cryptoRandomString({ length: 10 }), + totalChildrenCount: null, + sourceApplication: 'test suite', + streamId: cryptoRandomString({ length: 10 }), + branchId: cryptoRandomString({ length: 10 }), + branchName: cryptoRandomString({ length: 10 }) + }), + automationTokenGetter: async () => null + })({ + revisionId: cryptoRandomString({ length: 10 }), + manifest }) - ) - expect(e.message).to.eq('Automation not found') + throw 'this should have thrown' + } catch (error) { + if (!(error instanceof Error)) throw error + expect(error.message).contains('Cannot find a token for the automation') + } + }) }) - it('fails if project id is mismatched from automation id', async () => { - const trigger = buildManuallyTriggerAutomation() + describe('Run triggered manually', () => { + const buildManuallyTriggerAutomation = ( + overrides?: Partial + ) => { + const trigger = manuallyTriggerAutomation({ + getAutomationTriggerDefinitions, + getAutomation, + getBranchLatestCommits, + triggerFunction: triggerAutomationRevisionRun({ + automateRunTrigger: async () => ({ + automationRunId: cryptoRandomString({ length: 10 }) + }), + getFunctionInputDecryptor: getFunctionInputDecryptor({ buildDecryptor }), + getEncryptionKeyPairFor + }), + ...(overrides || {}) + }) + return trigger + } - const e = await expectToThrow( - async () => - await trigger({ - automationId: createdAutomation.automation.id, - userId: testUser.id, - projectId: otherUserStream.id - }) - ) - expect(e.message).to.eq('Automation not found') - }) + it('fails if referring to nonexistent automation', async () => { + const trigger = buildManuallyTriggerAutomation() - it('fails if revision has no version creation triggers', async () => { - const trigger = buildManuallyTriggerAutomation({ - getAutomationTriggerDefinitions: async () => [] + const e = await expectToThrow( + async () => + await trigger({ + automationId: cryptoRandomString({ length: 10 }), + userId: testUser.id, + projectId: testUserStream.id + }) + ) + expect(e.message).to.eq('Automation not found') }) - const e = await expectToThrow( - async () => - await trigger({ - automationId: createdAutomation.automation.id, - userId: testUser.id, - projectId: testUserStream.id - }) - ) - expect(e.message).to.eq( - 'No model version creation triggers found for the automation' - ) - }) - - it('fails if user does not have access to automation', async () => { - const trigger = buildManuallyTriggerAutomation() - - const e = await expectToThrow( - async () => - await trigger({ - automationId: createdAutomation.automation.id, - userId: otherUser.id, - projectId: testUserStream.id - }) - ) - expect(e.message).to.eq('User does not have required access to stream') - }) - - it('fails if no versions found for any triggers', async () => { - const trigger = buildManuallyTriggerAutomation() + it('fails if project id is mismatched from automation id', async () => { + const trigger = buildManuallyTriggerAutomation() - const e = await expectToThrow( - async () => - await trigger({ - automationId: createdAutomation.automation.id, - userId: testUser.id, - projectId: testUserStream.id - }) - ) - expect(e.message).to.eq( - 'No version to trigger on found for the available triggers' - ) - }) + const e = await expectToThrow( + async () => + await trigger({ + automationId: createdAutomation.automation.id, + userId: testUser.id, + projectId: otherUserStream.id + }) + ) + expect(e.message).to.eq('Automation not found') + }) - describe('with valid versions available', () => { - beforeEach(async () => { - await createTestCommit({ - id: '', - objectId: '', - streamId: testUserStream.id, - authorId: testUser.id + it('fails if revision has no version creation triggers', async () => { + const trigger = buildManuallyTriggerAutomation({ + getAutomationTriggerDefinitions: async () => [] }) - }) - afterEach(async () => { - await truncateTables([Commits.name]) - await Promise.all([ - updateAutomation({ - id: createdAutomation.automation.id, - enabled: true - }), - updateAutomationRevision({ - id: createdRevision.id, - active: true - }) - ]) + const e = await expectToThrow( + async () => + await trigger({ + automationId: createdAutomation.automation.id, + userId: testUser.id, + projectId: testUserStream.id + }) + ) + expect(e.message).to.eq( + 'No model version creation triggers found for the automation' + ) }) - it('fails if automation is disabled', async () => { - await updateAutomation({ - id: createdAutomation.automation.id, - enabled: false - }) - + it('fails if user does not have access to automation', async () => { const trigger = buildManuallyTriggerAutomation() const e = await expectToThrow( async () => await trigger({ automationId: createdAutomation.automation.id, - userId: testUser.id, + userId: otherUser.id, projectId: testUserStream.id }) ) - expect(e.message).to.eq('The automation is not enabled, cannot trigger it') + expect(e.message).to.eq('User does not have required access to stream') }) - it('fails if automation revision is disabled', async () => { - await updateAutomationRevision({ - id: createdRevision.id, - active: false - }) - + it('fails if no versions found for any triggers', async () => { const trigger = buildManuallyTriggerAutomation() const e = await expectToThrow( @@ -975,191 +916,258 @@ const { FF_AUTOMATE_MODULE_ENABLED } = Environment.getFeatureFlags() }) ) expect(e.message).to.eq( - 'No model version creation triggers found for the automation' + 'No version to trigger on found for the available triggers' ) }) - it('succeeds', async () => { - const trigger = buildManuallyTriggerAutomation() + describe('with valid versions available', () => { + beforeEach(async () => { + await createTestCommit({ + id: '', + objectId: '', + streamId: testUserStream.id, + authorId: testUser.id + }) + }) - const { automationRunId } = await trigger({ - automationId: createdAutomation.automation.id, - userId: testUser.id, - projectId: testUserStream.id + afterEach(async () => { + await truncateTables([Commits.name]) + await Promise.all([ + updateAutomation({ + id: createdAutomation.automation.id, + enabled: true + }), + updateAutomationRevision({ + id: createdRevision.id, + active: true + }) + ]) }) - const storedRun = await getFullAutomationRunById(automationRunId) - expect(storedRun).to.be.ok + it('fails if automation is disabled', async () => { + await updateAutomation({ + id: createdAutomation.automation.id, + enabled: false + }) - const expectedStatus = 'pending' - expect(storedRun!.status).to.equal(expectedStatus) - for (const run of storedRun!.functionRuns) { - expect(run.status).to.equal(expectedStatus) - } - }) - }) - }) + const trigger = buildManuallyTriggerAutomation() - describe('Existing automation run', () => { - let automationRun: InsertableAutomationRun + const e = await expectToThrow( + async () => + await trigger({ + automationId: createdAutomation.automation.id, + userId: testUser.id, + projectId: testUserStream.id + }) + ) + expect(e.message).to.eq('The automation is not enabled, cannot trigger it') + }) - before(async () => { - // Insert automation run directly to DB - automationRun = { - id: cryptoRandomString({ length: 10 }), - automationRevisionId: createdRevision.id, - createdAt: new Date(), - updatedAt: new Date(), - status: AutomationRunStatuses.running, - executionEngineRunId: cryptoRandomString({ length: 10 }), - triggers: [ - { - triggeringId: testUserStreamModel.id, - triggerType: VersionCreationTriggerType - } - ], - functionRuns: [ - { - functionId: generateFunctionId(), - functionReleaseId: generateFunctionReleaseId(), - id: cryptoRandomString({ length: 15 }), - status: AutomationRunStatuses.running, - elapsed: 0, - results: null, - contextView: null, - statusMessage: null, - createdAt: new Date(), - updatedAt: new Date() - } - ] - } + it('fails if automation revision is disabled', async () => { + await updateAutomationRevision({ + id: createdRevision.id, + active: false + }) - await upsertAutomationRun(automationRun) - }) + const trigger = buildManuallyTriggerAutomation() - describe('status update report', () => { - const buildReportFunctionRunStatus = () => { - const report = reportFunctionRunStatus({ - getAutomationFunctionRunRecord: getFunctionRun, - upsertAutomationFunctionRunRecord: upsertAutomationFunctionRun, - automationRunUpdater: updateAutomationRun + const e = await expectToThrow( + async () => + await trigger({ + automationId: createdAutomation.automation.id, + userId: testUser.id, + projectId: testUserStream.id + }) + ) + expect(e.message).to.eq( + 'No model version creation triggers found for the automation' + ) }) - return report - } + it('succeeds', async () => { + const trigger = buildManuallyTriggerAutomation() - it('fails fn with invalid functionRunId', async () => { - const report = buildReportFunctionRunStatus() + const { automationRunId } = await trigger({ + automationId: createdAutomation.automation.id, + userId: testUser.id, + projectId: testUserStream.id + }) - const functionRunId = 'nonexistent' - const params: Parameters[0] = { - runId: functionRunId, - status: mapGqlStatusToDbStatus(AutomateRunStatus.Succeeded), - statusMessage: null, - results: null, - contextView: null - } + const storedRun = await getFullAutomationRunById(automationRunId) + expect(storedRun).to.be.ok - expect(report(params)).to.eventually.be.rejectedWith('Function not found') + const expectedStatus = 'pending' + expect(storedRun!.status).to.equal(expectedStatus) + for (const run of storedRun!.functionRuns) { + expect(run.status).to.equal(expectedStatus) + } + }) }) + }) - it('fails fn with invalid status', async () => { - const report = buildReportFunctionRunStatus() - - const functionRunId = automationRun.functionRuns[0].id - const params: Parameters[0] = { - runId: functionRunId, - status: mapGqlStatusToDbStatus(AutomateRunStatus.Pending), - statusMessage: null, - results: null, - contextView: null + describe('Existing automation run', () => { + let automationRun: InsertableAutomationRun + + before(async () => { + // Insert automation run directly to DB + automationRun = { + id: cryptoRandomString({ length: 10 }), + automationRevisionId: createdRevision.id, + createdAt: new Date(), + updatedAt: new Date(), + status: AutomationRunStatuses.running, + executionEngineRunId: cryptoRandomString({ length: 10 }), + triggers: [ + { + triggeringId: testUserStreamModel.id, + triggerType: VersionCreationTriggerType + } + ], + functionRuns: [ + { + functionId: generateFunctionId(), + functionReleaseId: generateFunctionReleaseId(), + // runId: cryptoRandomString({ length: 15 }), + id: cryptoRandomString({ length: 15 }), + status: AutomationRunStatuses.running, + elapsed: 0, + results: null, + contextView: null, + statusMessage: null, + createdAt: new Date(), + updatedAt: new Date() + } + ] } - expect(report(params)).to.eventually.be.rejectedWith('Invalid status change') + await upsertAutomationRun(automationRun) }) - ;[ - { val: 1, error: 'invalid type' }, - { - val: { - version: '1.0', - values: { objectResults: [] }, - error: 'invalid version' - } - }, - { - val: { - version: 1.0, - values: {} - }, - error: 'invalid values object' - }, - { - val: { version: 1.0, values: { objectResults: [1] } }, - error: 'invalid objectResults item type' - }, - { - val: { version: 1.0, values: { objectResults: [{}] } }, - error: 'invalid objectResults item keys' + + describe('status update report', () => { + const buildReportFunctionRunStatus = () => { + const report = reportFunctionRunStatus({ + getAutomationFunctionRunRecord: getFunctionRun, + upsertAutomationFunctionRunRecord: upsertAutomationFunctionRun, + automationRunUpdater: updateAutomationRun + }) + + return report } - ].forEach(({ val, error }) => { - it('fails fn with invalid results: ' + error, async () => { + + it('fails fn with invalid functionRunId', async () => { const report = buildReportFunctionRunStatus() - const functionRunId = automationRun.functionRuns[0].id + const functionRunId = 'nonexistent' const params: Parameters[0] = { runId: functionRunId, status: mapGqlStatusToDbStatus(AutomateRunStatus.Succeeded), statusMessage: null, - results: val as unknown as Automate.AutomateTypes.ResultsSchema, + results: null, contextView: null } - expect(report(params)).to.eventually.be.rejectedWith( - 'Invalid results schema' - ) + await expect(report(params)).to.eventually.be + .rejectedWith(FunctionRunNotFoundError) }) - }) - it('fails fn with invalid contextView url', async () => { - const report = buildReportFunctionRunStatus() + it('fails fn with invalid status', async () => { + const report = buildReportFunctionRunStatus() - const functionRunId = automationRun.functionRuns[0].id - const params: Parameters[0] = { - runId: functionRunId, - status: mapGqlStatusToDbStatus(AutomateRunStatus.Succeeded), - statusMessage: null, - results: null, - contextView: 'invalid-url' - } + const functionRunId = automationRun.functionRuns[0].runId + const params: Parameters[0] = { + runId: functionRunId, + status: mapGqlStatusToDbStatus(AutomateRunStatus.Pending), + statusMessage: null, + results: null, + contextView: null + } - expect(report(params)).to.eventually.be.rejectedWith('Invalid contextView') - }) + await expect(report(params)).to.eventually.be + .rejectedWith(FunctionRunReportStatusError, /^Invalid status change/) + }) + ;[ + { val: 1, error: 'invalid type' }, + { + val: { + version: '1.0', + values: { objectResults: [] }, + error: 'invalid version' + } + }, + { + val: { + version: 1.0, + values: {} + }, + error: 'invalid values object' + }, + { + val: { version: 1.0, values: { objectResults: [1] } }, + error: 'invalid objectResults item type' + }, + { + val: { version: 1.0, values: { objectResults: [{}] } }, + error: 'invalid objectResults item keys' + } + ].forEach(({ val, error }) => { + it('fails fn with invalid results: ' + error, async () => { + const report = buildReportFunctionRunStatus() + + const functionRunId = automationRun.functionRuns[0].id + const params: Parameters[0] = { + runId: functionRunId, + status: mapGqlStatusToDbStatus(AutomateRunStatus.Succeeded), + statusMessage: null, + results: val as unknown as Automate.AutomateTypes.ResultsSchema, + contextView: null + } + + await expect(report(params)).to.eventually.be + .rejectedWith(Automate.UnformattableResultsSchemaError, 'Invalid results schema') + }) + }) - it('succeeds', async () => { - const report = buildReportFunctionRunStatus() - - const functionRunId = automationRun.functionRuns[0].id - const contextView = '/a/b/c' - const params: Parameters[0] = { - runId: functionRunId, - status: mapGqlStatusToDbStatus(AutomateRunStatus.Succeeded), - statusMessage: null, - results: null, - contextView - } + it('fails fn with invalid contextView url', async () => { + const report = buildReportFunctionRunStatus() - expect(report(params)).to.eventually.be.true + const functionRunId = automationRun.functionRuns[0].id + const params: Parameters[0] = { + runId: functionRunId, + status: mapGqlStatusToDbStatus(AutomateRunStatus.Succeeded), + statusMessage: null, + results: null, + contextView: 'invalid-url' + } - const [updatedRun, updatedFnRun] = await Promise.all([ - getFullAutomationRunById(automationRun.id), - getFunctionRun(functionRunId) - ]) + await expect(report(params)).to.eventually.be + .rejectedWith(FunctionRunReportStatusError, 'Context view must start with a forward slash') + }) + + it('succeeds', async () => { + const report = buildReportFunctionRunStatus() - expect(updatedRun?.status).to.equal(AutomationRunStatuses.succeeded) - expect(updatedFnRun?.status).to.equal(AutomationRunStatuses.succeeded) - expect(updatedFnRun?.contextView).to.equal(contextView) + const functionRunId = automationRun.functionRuns[0].id + const contextView = '/a/b/c' + const params: Parameters[0] = { + runId: functionRunId, + status: mapGqlStatusToDbStatus(AutomateRunStatus.Succeeded), + statusMessage: null, + results: null, + contextView + } + + await expect(report(params)).to.eventually.be.true + + const [updatedRun, updatedFnRun] = await Promise.all([ + getFullAutomationRunById(automationRun.id), + getFunctionRun(functionRunId) + ]) + + expect(updatedRun?.status).to.equal(AutomationRunStatuses.succeeded) + expect(updatedFnRun?.status).to.equal(AutomationRunStatuses.succeeded) + expect(updatedFnRun?.contextView).to.equal(contextView) + }) }) }) - }) - } -) + } + ) From e0372f5e29b109a370c0dfb3b7528fc1a6e05d45 Mon Sep 17 00:00:00 2001 From: Chuck Driesler Date: Thu, 16 May 2024 14:13:04 +0100 Subject: [PATCH 9/9] use correct run ids, adjust tests --- .../automate/clients/executionEngine.ts | 2 +- .../automate/repositories/automations.ts | 27 +- .../automate/services/runsManagement.ts | 94 +- .../modules/automate/tests/trigger.spec.ts | 1926 +++++++++-------- 4 files changed, 1025 insertions(+), 1024 deletions(-) diff --git a/packages/server/modules/automate/clients/executionEngine.ts b/packages/server/modules/automate/clients/executionEngine.ts index b6023a3307..69ff304e36 100644 --- a/packages/server/modules/automate/clients/executionEngine.ts +++ b/packages/server/modules/automate/clients/executionEngine.ts @@ -169,7 +169,7 @@ export const triggerAutomationRun = async (params: { functionId: functionRun.functionId, functionReleaseId: functionRun.functionReleaseId, functionInputs: functionRun.functionInputs, - functionRunId: functionRun.runId + functionRunId: functionRun.id } }) diff --git a/packages/server/modules/automate/repositories/automations.ts b/packages/server/modules/automate/repositories/automations.ts index 4006ced91d..58d3342340 100644 --- a/packages/server/modules/automate/repositories/automations.ts +++ b/packages/server/modules/automate/repositories/automations.ts @@ -153,7 +153,6 @@ export async function upsertAutomationRun(automationRun: InsertableAutomationRun } export async function getFunctionRun(functionRunId: string) { - console.log({ functionRunId }) const q = AutomationFunctionRuns.knex() .select< Array< @@ -265,11 +264,11 @@ export async function getFullAutomationRunById( return run ? { - ...formatJsonArrayRecords(run.runs)[0], - triggers: formatJsonArrayRecords(run.triggers), - functionRuns: formatJsonArrayRecords(run.functionRuns), - automationId: run.automationId - } + ...formatJsonArrayRecords(run.runs)[0], + triggers: formatJsonArrayRecords(run.triggers), + functionRuns: formatJsonArrayRecords(run.functionRuns), + automationId: run.automationId + } : null } @@ -353,11 +352,11 @@ export async function storeAutomationRevision(revision: InsertableAutomationRevi // Unset 'active in revision' for all other revisions ...(revision.active ? [ - AutomationRevisions.knex() - .where(AutomationRevisions.col.automationId, newRev.automationId) - .andWhereNot(AutomationRevisions.col.id, newRev.id) - .update(AutomationRevisions.withoutTablePrefix.col.active, false) - ] + AutomationRevisions.knex() + .where(AutomationRevisions.col.automationId, newRev.automationId) + .andWhereNot(AutomationRevisions.col.id, newRev.id) + .update(AutomationRevisions.withoutTablePrefix.col.active, false) + ] : []) ]) @@ -838,9 +837,9 @@ export const getAutomationProjects = async (params: { Automations.colAs('id', 'automationId'), ...(userId ? [ - // Getting first role from grouped results - knex.raw(`(array_agg("stream_acl"."role"))[1] as role`) - ] + // Getting first role from grouped results + knex.raw(`(array_agg("stream_acl"."role"))[1] as role`) + ] : []) ]) .whereIn(Automations.col.id, automationIds) diff --git a/packages/server/modules/automate/services/runsManagement.ts b/packages/server/modules/automate/services/runsManagement.ts index c1282acbe2..bec9b53452 100644 --- a/packages/server/modules/automate/services/runsManagement.ts +++ b/packages/server/modules/automate/services/runsManagement.ts @@ -51,9 +51,7 @@ export const validateStatusChange = ( export const validateContextView = (contextView: string) => { if (!contextView.length) { - throw new FunctionRunReportStatusError( - 'Context view must be a valid relative URL' - ) + throw new FunctionRunReportStatusError('Context view must be a valid relative URL') } if (!contextView.startsWith('/')) { @@ -98,59 +96,59 @@ export type ReportFunctionRunStatusDeps = { export const reportFunctionRunStatus = (deps: ReportFunctionRunStatusDeps) => - async ( - params: Pick< - AutomationFunctionRunRecord, - 'runId' | 'status' | 'statusMessage' | 'contextView' | 'results' - > - ): Promise => { - const { - getAutomationFunctionRunRecord, - upsertAutomationFunctionRunRecord, - automationRunUpdater - } = deps - const { runId, ...statusReportData } = params - - const currentFunctionRunRecordResult = await getAutomationFunctionRunRecord(runId) - - if (!currentFunctionRunRecordResult) { - throw new FunctionRunNotFoundError() - } + async ( + params: Pick< + AutomationFunctionRunRecord, + 'runId' | 'status' | 'statusMessage' | 'contextView' | 'results' + > + ): Promise => { + const { + getAutomationFunctionRunRecord, + upsertAutomationFunctionRunRecord, + automationRunUpdater + } = deps + const { runId, ...statusReportData } = params + + const currentFunctionRunRecordResult = await getAutomationFunctionRunRecord(runId) + + if (!currentFunctionRunRecordResult) { + throw new FunctionRunNotFoundError() + } - const { automationId, ...currentFunctionRunRecord } = currentFunctionRunRecordResult + const { automationId, ...currentFunctionRunRecord } = currentFunctionRunRecordResult - if (statusReportData.results) { - Automate.AutomateTypes.formatResultsSchema(statusReportData.results) - } + if (statusReportData.results) { + Automate.AutomateTypes.formatResultsSchema(statusReportData.results) + } - if (statusReportData.contextView) validateContextView(statusReportData.contextView) + if (statusReportData.contextView) validateContextView(statusReportData.contextView) - const currentStatus = currentFunctionRunRecord.status - const nextStatus = statusReportData.status + const currentStatus = currentFunctionRunRecord.status + const nextStatus = statusReportData.status - validateStatusChange(currentStatus, nextStatus) + validateStatusChange(currentStatus, nextStatus) - const elapsed = new Date().getTime() - currentFunctionRunRecord.createdAt.getTime() + const elapsed = new Date().getTime() - currentFunctionRunRecord.createdAt.getTime() - const nextFunctionRunRecord = { - ...currentFunctionRunRecord, - ...statusReportData, - elapsed - } + const nextFunctionRunRecord = { + ...currentFunctionRunRecord, + ...statusReportData, + elapsed + } - await upsertAutomationFunctionRunRecord(nextFunctionRunRecord) + await upsertAutomationFunctionRunRecord(nextFunctionRunRecord) - const updatedRun = await automationRunUpdater({ - id: runId, - status: resolveStatusFromFunctionRunStatuses([nextStatus]), - updatedAt: new Date() - }) + const updatedRun = await automationRunUpdater({ + id: currentFunctionRunRecord.runId, + status: resolveStatusFromFunctionRunStatuses([nextStatus]), + updatedAt: new Date() + }) - await AutomateRunsEmitter.emit(AutomateRunsEmitter.events.StatusUpdated, { - run: updatedRun, - functionRuns: [nextFunctionRunRecord], - automationId - }) + await AutomateRunsEmitter.emit(AutomateRunsEmitter.events.StatusUpdated, { + run: updatedRun, + functionRuns: [nextFunctionRunRecord], + automationId + }) - return true - } + return true + } diff --git a/packages/server/modules/automate/tests/trigger.spec.ts b/packages/server/modules/automate/tests/trigger.spec.ts index 84e8dc7e9e..f9b743d9a0 100644 --- a/packages/server/modules/automate/tests/trigger.spec.ts +++ b/packages/server/modules/automate/tests/trigger.spec.ts @@ -65,846 +65,909 @@ import { } from '@/modules/automate/services/encryption' import { buildDecryptor } from '@/modules/shared/utils/libsodium' import { mapGqlStatusToDbStatus } from '@/modules/automate/utils/automateFunctionRunStatus' -import { Automate } from '@speckle/shared' const { FF_AUTOMATE_MODULE_ENABLED } = Environment.getFeatureFlags() - ; (FF_AUTOMATE_MODULE_ENABLED ? describe : describe.skip)( - 'Automate triggers @automate', - () => { - const testUser: BasicTestUser = { - id: cryptoRandomString({ length: 10 }), - name: 'The Automaton', - email: 'the@automaton.com' - } - - const otherUser: BasicTestUser = { - id: cryptoRandomString({ length: 10 }), - name: 'The Automaton Other', - email: 'theother@automaton.com' - } - - const testUserStream: BasicTestStream = { - id: '', - name: 'First stream', - isPublic: true, - ownerId: '' - } - - const otherUserStream: BasicTestStream = { - id: '', - name: 'Other stream', - isPublic: true, - ownerId: '' - } - - let testUserStreamModel: BranchRecord - let createdAutomation: Awaited>> - let createdRevision: Awaited< - ReturnType> - > - let publicKey: string - - before(async () => { - await beforeEachContext() - await createTestUsers([testUser, otherUser]) - publicKey = await getEncryptionPublicKey() +;(FF_AUTOMATE_MODULE_ENABLED ? describe : describe.skip)( + 'Automate triggers @automate', + () => { + const testUser: BasicTestUser = { + id: cryptoRandomString({ length: 10 }), + name: 'The Automaton', + email: 'the@automaton.com' + } - const createAutomation = buildAutomationCreate() - const createRevision = buildAutomationRevisionCreate() + const otherUser: BasicTestUser = { + id: cryptoRandomString({ length: 10 }), + name: 'The Automaton Other', + email: 'theother@automaton.com' + } - await createTestStreams([ - [testUserStream, testUser], - [otherUserStream, otherUser] - ]) + const testUserStream: BasicTestStream = { + id: '', + name: 'First stream', + isPublic: true, + ownerId: '' + } - const [projectModel, newAutomation] = await Promise.all([ - getLatestStreamBranch(testUserStream.id), - createAutomation({ - userId: testUser.id, - projectId: testUserStream.id, - input: { - name: 'Manually Triggerable Automation', - enabled: true - } - }) - ]) - testUserStreamModel = projectModel - createdAutomation = newAutomation + const otherUserStream: BasicTestStream = { + id: '', + name: 'Other stream', + isPublic: true, + ownerId: '' + } - createdRevision = await createRevision({ + let testUserStreamModel: BranchRecord + let createdAutomation: Awaited>> + let createdRevision: Awaited< + ReturnType> + > + let publicKey: string + + before(async () => { + await beforeEachContext() + await createTestUsers([testUser, otherUser]) + publicKey = await getEncryptionPublicKey() + + const createAutomation = buildAutomationCreate() + const createRevision = buildAutomationRevisionCreate() + + await createTestStreams([ + [testUserStream, testUser], + [otherUserStream, otherUser] + ]) + + const [projectModel, newAutomation] = await Promise.all([ + getLatestStreamBranch(testUserStream.id), + createAutomation({ userId: testUser.id, + projectId: testUserStream.id, input: { - automationId: createdAutomation.automation.id, - triggerDefinitions: { - version: 1.0, - definitions: [{ type: 'VERSION_CREATED', modelId: testUserStreamModel.id }] - }, - functions: [ - { - functionReleaseId: generateFunctionReleaseId(), - functionId: generateFunctionId(), - parameters: null - } - ] - }, - projectId: testUserStream.id - }) - - expect(createdRevision).to.be.ok - }) - describe('On model version create', () => { - it('No trigger no run', async () => { - const triggered: Record = {} - await onModelVersionCreate({ - getTriggers: async () => [], - triggerFunction: async ({ manifest, revisionId }) => { - triggered[revisionId] = manifest - return { automationRunId: cryptoRandomString({ length: 10 }) } - } - })({ - modelId: cryptoRandomString({ length: 10 }), - versionId: cryptoRandomString({ length: 10 }), - projectId: cryptoRandomString({ length: 10 }) - }) - expect(Object.keys(triggered)).length(0) - }) - it('Triggers all automation runs associated with the model', async () => { - const storedTriggers: AutomationTriggerDefinitionRecord< - typeof VersionCreationTriggerType - >[] = [ - { - triggerType: VersionCreationTriggerType, - triggeringId: cryptoRandomString({ length: 10 }), - automationRevisionId: cryptoRandomString({ length: 10 }) - }, - { - triggerType: VersionCreationTriggerType, - triggeringId: cryptoRandomString({ length: 10 }), - automationRevisionId: cryptoRandomString({ length: 10 }) - } - ] - const triggered: Record = {} - const versionId = cryptoRandomString({ length: 10 }) - const projectId = cryptoRandomString({ length: 10 }) - - await onModelVersionCreate({ - getTriggers: async < - T extends AutomationTriggerType = AutomationTriggerType - >() => storedTriggers as AutomationTriggerDefinitionRecord[], - triggerFunction: async ({ revisionId, manifest }) => { - if (!isVersionCreatedTriggerManifest(manifest)) { - throw new Error('unexpected trigger type') - } - - triggered[revisionId] = manifest - return { automationRunId: cryptoRandomString({ length: 10 }) } - } - })({ - modelId: cryptoRandomString({ length: 10 }), - versionId, - projectId - }) - expect(Object.keys(triggered)).length(storedTriggers.length) - storedTriggers.forEach((st) => { - const expectedTrigger: VersionCreatedTriggerManifest = { - versionId, - modelId: st.triggeringId, - triggerType: st.triggerType, - projectId - } - expect(triggered[st.automationRevisionId]).deep.equal(expectedTrigger) - }) + name: 'Manually Triggerable Automation', + enabled: true + } }) - it('Failing automation runs do NOT break other runs.', async () => { - const storedTriggers: AutomationTriggerDefinitionRecord[] = [ - { - triggerType: VersionCreationTriggerType, - triggeringId: cryptoRandomString({ length: 10 }), - automationRevisionId: cryptoRandomString({ length: 10 }) - }, + ]) + testUserStreamModel = projectModel + createdAutomation = newAutomation + + createdRevision = await createRevision({ + userId: testUser.id, + input: { + automationId: createdAutomation.automation.id, + triggerDefinitions: { + version: 1.0, + definitions: [{ type: 'VERSION_CREATED', modelId: testUserStreamModel.id }] + }, + functions: [ { - triggerType: VersionCreationTriggerType, - triggeringId: cryptoRandomString({ length: 10 }), - automationRevisionId: cryptoRandomString({ length: 10 }) + functionReleaseId: generateFunctionReleaseId(), + functionId: generateFunctionId(), + parameters: null } ] - const triggered: Record = {} - const versionId = cryptoRandomString({ length: 10 }) - await onModelVersionCreate({ - getTriggers: async < - T extends AutomationTriggerType = AutomationTriggerType - >() => storedTriggers as AutomationTriggerDefinitionRecord[], - triggerFunction: async ({ revisionId, manifest }) => { - if (!isVersionCreatedTriggerManifest(manifest)) { - throw new Error('unexpected trigger type') - } - if (revisionId === storedTriggers[0].automationRevisionId) - throw new Error('first one is borked') - - triggered[revisionId] = manifest - return { automationRunId: cryptoRandomString({ length: 10 }) } - } - })({ - modelId: cryptoRandomString({ length: 10 }), - versionId, - projectId: cryptoRandomString({ length: 10 }) - }) - expect(Object.keys(triggered)).length(storedTriggers.length - 1) - }) + }, + projectId: testUserStream.id }) - describe('Triggering an automation revision run', () => { - it('Throws if run conditions are not met', async () => { - try { - await triggerAutomationRevisionRun({ - automateRunTrigger: async () => ({ - automationRunId: cryptoRandomString({ length: 10 }) - }), - getFunctionInputDecryptor: getFunctionInputDecryptor({ buildDecryptor }), - getEncryptionKeyPairFor - })({ - revisionId: cryptoRandomString({ length: 10 }), - manifest: { - versionId: cryptoRandomString({ length: 10 }), - triggerType: VersionCreationTriggerType, - modelId: cryptoRandomString({ length: 10 }) - } - }) - throw 'this should have thrown' - } catch (error) { - if (!(error instanceof Error)) throw error - expect(error.message).contains( - "Cannot trigger the given revision, it doesn't exist" - ) + + expect(createdRevision).to.be.ok + }) + describe('On model version create', () => { + it('No trigger no run', async () => { + const triggered: Record = {} + await onModelVersionCreate({ + getTriggers: async () => [], + triggerFunction: async ({ manifest, revisionId }) => { + triggered[revisionId] = manifest + return { automationRunId: cryptoRandomString({ length: 10 }) } } + })({ + modelId: cryptoRandomString({ length: 10 }), + versionId: cryptoRandomString({ length: 10 }), + projectId: cryptoRandomString({ length: 10 }) }) - it('Saves run with an error if automate run trigger fails', async () => { - const userId = testUser.id - - const project = { - name: cryptoRandomString({ length: 10 }), - id: cryptoRandomString({ length: 10 }), - ownerId: userId, - isPublic: true + expect(Object.keys(triggered)).length(0) + }) + it('Triggers all automation runs associated with the model', async () => { + const storedTriggers: AutomationTriggerDefinitionRecord< + typeof VersionCreationTriggerType + >[] = [ + { + triggerType: VersionCreationTriggerType, + triggeringId: cryptoRandomString({ length: 10 }), + automationRevisionId: cryptoRandomString({ length: 10 }) + }, + { + triggerType: VersionCreationTriggerType, + triggeringId: cryptoRandomString({ length: 10 }), + automationRevisionId: cryptoRandomString({ length: 10 }) } + ] + const triggered: Record = {} + const versionId = cryptoRandomString({ length: 10 }) + const projectId = cryptoRandomString({ length: 10 }) + + await onModelVersionCreate({ + getTriggers: async < + T extends AutomationTriggerType = AutomationTriggerType + >() => storedTriggers as AutomationTriggerDefinitionRecord[], + triggerFunction: async ({ revisionId, manifest }) => { + if (!isVersionCreatedTriggerManifest(manifest)) { + throw new Error('unexpected trigger type') + } - await createTestStream(project, testUser) - const version = { - id: cryptoRandomString({ length: 10 }), - streamId: project.id, - objectId: null, - authorId: userId + triggered[revisionId] = manifest + return { automationRunId: cryptoRandomString({ length: 10 }) } } - // @ts-expect-error force setting the objectId to null - await createTestCommit(version) - // create automation, - const automation = { - id: cryptoRandomString({ length: 10 }), - createdAt: new Date(), - updatedAt: new Date(), - name: cryptoRandomString({ length: 15 }), - enabled: true, - projectId: project.id, - executionEngineAutomationId: cryptoRandomString({ length: 10 }), - userId + })({ + modelId: cryptoRandomString({ length: 10 }), + versionId, + projectId + }) + expect(Object.keys(triggered)).length(storedTriggers.length) + storedTriggers.forEach((st) => { + const expectedTrigger: VersionCreatedTriggerManifest = { + versionId, + modelId: st.triggeringId, + triggerType: st.triggerType, + projectId } - const automationToken = { - automationId: automation.id, - automateToken: cryptoRandomString({ length: 10 }), - automateRefreshToken: cryptoRandomString({ length: 10 }) + expect(triggered[st.automationRevisionId]).deep.equal(expectedTrigger) + }) + }) + it('Failing automation runs do NOT break other runs.', async () => { + const storedTriggers: AutomationTriggerDefinitionRecord[] = [ + { + triggerType: VersionCreationTriggerType, + triggeringId: cryptoRandomString({ length: 10 }), + automationRevisionId: cryptoRandomString({ length: 10 }) + }, + { + triggerType: VersionCreationTriggerType, + triggeringId: cryptoRandomString({ length: 10 }), + automationRevisionId: cryptoRandomString({ length: 10 }) } - await storeAutomation(automation, automationToken) + ] + const triggered: Record = {} + const versionId = cryptoRandomString({ length: 10 }) + await onModelVersionCreate({ + getTriggers: async < + T extends AutomationTriggerType = AutomationTriggerType + >() => storedTriggers as AutomationTriggerDefinitionRecord[], + triggerFunction: async ({ revisionId, manifest }) => { + if (!isVersionCreatedTriggerManifest(manifest)) { + throw new Error('unexpected trigger type') + } + if (revisionId === storedTriggers[0].automationRevisionId) + throw new Error('first one is borked') - const automationRevisionId = cryptoRandomString({ length: 10 }) - const trigger = { - triggerType: VersionCreationTriggerType, - triggeringId: cryptoRandomString({ length: 10 }) + triggered[revisionId] = manifest + return { automationRunId: cryptoRandomString({ length: 10 }) } } - // create revision, - await storeAutomationRevision({ - id: automationRevisionId, - createdAt: new Date(), - automationId: automation.id, - active: true, - triggers: [trigger], - userId, - publicKey, - functions: [ - { - functionInputs: null, - functionReleaseId: cryptoRandomString({ length: 10 }), - functionId: cryptoRandomString({ length: 10 }) - } - ] - }) - const thrownError = 'trigger failed' - const { automationRunId } = await triggerAutomationRevisionRun({ - automateRunTrigger: async () => { - throw new Error(thrownError) - }, + })({ + modelId: cryptoRandomString({ length: 10 }), + versionId, + projectId: cryptoRandomString({ length: 10 }) + }) + expect(Object.keys(triggered)).length(storedTriggers.length - 1) + }) + }) + describe('Triggering an automation revision run', () => { + it('Throws if run conditions are not met', async () => { + try { + await triggerAutomationRevisionRun({ + automateRunTrigger: async () => ({ + automationRunId: cryptoRandomString({ length: 10 }) + }), getFunctionInputDecryptor: getFunctionInputDecryptor({ buildDecryptor }), getEncryptionKeyPairFor })({ - revisionId: automationRevisionId, + revisionId: cryptoRandomString({ length: 10 }), manifest: { - versionId: version.id, - modelId: trigger.triggeringId, - triggerType: trigger.triggerType + versionId: cryptoRandomString({ length: 10 }), + triggerType: VersionCreationTriggerType, + modelId: cryptoRandomString({ length: 10 }) } }) + throw 'this should have thrown' + } catch (error) { + if (!(error instanceof Error)) throw error + expect(error.message).contains( + "Cannot trigger the given revision, it doesn't exist" + ) + } + }) + it('Saves run with an error if automate run trigger fails', async () => { + const userId = testUser.id + + const project = { + name: cryptoRandomString({ length: 10 }), + id: cryptoRandomString({ length: 10 }), + ownerId: userId, + isPublic: true + } - const storedRun = await getFullAutomationRunById(automationRunId) - if (!storedRun) throw 'cant fint the stored run' - - const expectedStatus = 'error' + await createTestStream(project, testUser) + const version = { + id: cryptoRandomString({ length: 10 }), + streamId: project.id, + objectId: null, + authorId: userId + } + // @ts-expect-error force setting the objectId to null + await createTestCommit(version) + // create automation, + const automation = { + id: cryptoRandomString({ length: 10 }), + createdAt: new Date(), + updatedAt: new Date(), + name: cryptoRandomString({ length: 15 }), + enabled: true, + projectId: project.id, + executionEngineAutomationId: cryptoRandomString({ length: 10 }), + userId + } + const automationToken = { + automationId: automation.id, + automateToken: cryptoRandomString({ length: 10 }), + automateRefreshToken: cryptoRandomString({ length: 10 }) + } + await storeAutomation(automation, automationToken) - expect(storedRun.status).to.equal(expectedStatus) - for (const run of storedRun.functionRuns) { - expect(run.status).to.equal(expectedStatus) - expect(run.statusMessage).to.equal(thrownError) + const automationRevisionId = cryptoRandomString({ length: 10 }) + const trigger = { + triggerType: VersionCreationTriggerType, + triggeringId: cryptoRandomString({ length: 10 }) + } + // create revision, + await storeAutomationRevision({ + id: automationRevisionId, + createdAt: new Date(), + automationId: automation.id, + active: true, + triggers: [trigger], + userId, + publicKey, + functions: [ + { + functionInputs: null, + functionReleaseId: cryptoRandomString({ length: 10 }), + functionId: cryptoRandomString({ length: 10 }) + } + ] + }) + const thrownError = 'trigger failed' + const { automationRunId } = await triggerAutomationRevisionRun({ + automateRunTrigger: async () => { + throw new Error(thrownError) + }, + getFunctionInputDecryptor: getFunctionInputDecryptor({ buildDecryptor }), + getEncryptionKeyPairFor + })({ + revisionId: automationRevisionId, + manifest: { + versionId: version.id, + modelId: trigger.triggeringId, + triggerType: trigger.triggerType } }) - it('Saves run with the execution engine run id if trigger is successful', async () => { - // create user, project, model, version - const userId = testUser.id + const storedRun = await getFullAutomationRunById(automationRunId) + if (!storedRun) throw 'cant fint the stored run' - const project = { - name: cryptoRandomString({ length: 10 }), - id: cryptoRandomString({ length: 10 }), - ownerId: userId, - isPublic: true - } + const expectedStatus = 'exception' - await createTestStream(project, testUser) - const version = { - id: cryptoRandomString({ length: 10 }), - streamId: project.id, - objectId: null, - authorId: userId - } - // @ts-expect-error force setting the objectId to null - await createTestCommit(version) - // create automation, - const automation = { - id: cryptoRandomString({ length: 10 }), - createdAt: new Date(), - updatedAt: new Date(), - name: cryptoRandomString({ length: 15 }), - enabled: true, - projectId: project.id, - executionEngineAutomationId: cryptoRandomString({ length: 10 }), - userId - } - const automationToken = { - automationId: automation.id, - automateToken: cryptoRandomString({ length: 10 }), - automateRefreshToken: cryptoRandomString({ length: 10 }) - } - await storeAutomation(automation, automationToken) + expect(storedRun.status).to.equal(expectedStatus) + for (const run of storedRun.functionRuns) { + expect(run.status).to.equal(expectedStatus) + expect(run.statusMessage).to.equal(thrownError) + } + }) + it('Saves run with the execution engine run id if trigger is successful', async () => { + // create user, project, model, version - const automationRevisionId = cryptoRandomString({ length: 10 }) - const trigger = { - triggerType: VersionCreationTriggerType, - triggeringId: cryptoRandomString({ length: 10 }) - } - // create revision, - await storeAutomationRevision({ - id: automationRevisionId, - createdAt: new Date(), - automationId: automation.id, - active: true, - triggers: [trigger], - userId, - publicKey, - functions: [ - { - functionInputs: null, - functionReleaseId: cryptoRandomString({ length: 10 }), - functionId: cryptoRandomString({ length: 10 }) - } - ] - }) - const executionEngineRunId = cryptoRandomString({ length: 10 }) - const { automationRunId } = await triggerAutomationRevisionRun({ - automateRunTrigger: async () => ({ - automationRunId: executionEngineRunId - }), - getFunctionInputDecryptor: getFunctionInputDecryptor({ buildDecryptor }), - getEncryptionKeyPairFor - })({ - revisionId: automationRevisionId, - manifest: { - versionId: version.id, - modelId: trigger.triggeringId, - triggerType: trigger.triggerType - } - }) + const userId = testUser.id - const storedRun = await getFullAutomationRunById(automationRunId) - if (!storedRun) throw 'cant fint the stored run' + const project = { + name: cryptoRandomString({ length: 10 }), + id: cryptoRandomString({ length: 10 }), + ownerId: userId, + isPublic: true + } - const expectedStatus = 'pending' + await createTestStream(project, testUser) + const version = { + id: cryptoRandomString({ length: 10 }), + streamId: project.id, + objectId: null, + authorId: userId + } + // @ts-expect-error force setting the objectId to null + await createTestCommit(version) + // create automation, + const automation = { + id: cryptoRandomString({ length: 10 }), + createdAt: new Date(), + updatedAt: new Date(), + name: cryptoRandomString({ length: 15 }), + enabled: true, + projectId: project.id, + executionEngineAutomationId: cryptoRandomString({ length: 10 }), + userId + } + const automationToken = { + automationId: automation.id, + automateToken: cryptoRandomString({ length: 10 }), + automateRefreshToken: cryptoRandomString({ length: 10 }) + } + await storeAutomation(automation, automationToken) - expect(storedRun.status).to.equal(expectedStatus) - expect(storedRun.executionEngineRunId).to.equal(executionEngineRunId) - for (const run of storedRun.functionRuns) { - expect(run.status).to.equal(expectedStatus) - } + const automationRevisionId = cryptoRandomString({ length: 10 }) + const trigger = { + triggerType: VersionCreationTriggerType, + triggeringId: cryptoRandomString({ length: 10 }) + } + // create revision, + await storeAutomationRevision({ + id: automationRevisionId, + createdAt: new Date(), + automationId: automation.id, + active: true, + triggers: [trigger], + userId, + publicKey, + functions: [ + { + functionInputs: null, + functionReleaseId: cryptoRandomString({ length: 10 }), + functionId: cryptoRandomString({ length: 10 }) + } + ] }) - }) - describe('Run conditions are NOT met if', () => { - it("the referenced revision doesn't exist", async () => { - try { - await ensureRunConditions({ - revisionGetter: async () => null, - versionGetter: async () => undefined, - automationTokenGetter: async () => null - })({ - revisionId: cryptoRandomString({ length: 10 }), - manifest: { - triggerType: VersionCreationTriggerType, - modelId: cryptoRandomString({ length: 10 }), - versionId: cryptoRandomString({ length: 10 }) - } - }) - throw 'this should have thrown' - } catch (error) { - if (!(error instanceof Error)) throw error - expect(error.message).contains( - "Cannot trigger the given revision, it doesn't exist" - ) + const executionEngineRunId = cryptoRandomString({ length: 10 }) + const { automationRunId } = await triggerAutomationRevisionRun({ + automateRunTrigger: async () => ({ + automationRunId: executionEngineRunId + }), + getFunctionInputDecryptor: getFunctionInputDecryptor({ buildDecryptor }), + getEncryptionKeyPairFor + })({ + revisionId: automationRevisionId, + manifest: { + versionId: version.id, + modelId: trigger.triggeringId, + triggerType: trigger.triggerType } }) - it('the automation is not enabled', async () => { - try { - await ensureRunConditions({ - revisionGetter: async () => ({ + + const storedRun = await getFullAutomationRunById(automationRunId) + if (!storedRun) throw 'cant fint the stored run' + + const expectedStatus = 'pending' + + expect(storedRun.status).to.equal(expectedStatus) + expect(storedRun.executionEngineRunId).to.equal(executionEngineRunId) + for (const run of storedRun.functionRuns) { + expect(run.status).to.equal(expectedStatus) + } + }) + }) + describe('Run conditions are NOT met if', () => { + it("the referenced revision doesn't exist", async () => { + try { + await ensureRunConditions({ + revisionGetter: async () => null, + versionGetter: async () => undefined, + automationTokenGetter: async () => null + })({ + revisionId: cryptoRandomString({ length: 10 }), + manifest: { + triggerType: VersionCreationTriggerType, + modelId: cryptoRandomString({ length: 10 }), + versionId: cryptoRandomString({ length: 10 }) + } + }) + throw 'this should have thrown' + } catch (error) { + if (!(error instanceof Error)) throw error + expect(error.message).contains( + "Cannot trigger the given revision, it doesn't exist" + ) + } + }) + it('the automation is not enabled', async () => { + try { + await ensureRunConditions({ + revisionGetter: async () => ({ + id: cryptoRandomString({ length: 10 }), + name: cryptoRandomString({ length: 10 }), + projectId: cryptoRandomString({ length: 10 }), + enabled: false, + createdAt: new Date(), + updatedAt: new Date(), + executionEngineAutomationId: cryptoRandomString({ length: 10 }), + userId: cryptoRandomString({ length: 10 }), + revision: { id: cryptoRandomString({ length: 10 }), - name: cryptoRandomString({ length: 10 }), - projectId: cryptoRandomString({ length: 10 }), - enabled: false, createdAt: new Date(), - updatedAt: new Date(), - executionEngineAutomationId: cryptoRandomString({ length: 10 }), + publicKey, userId: cryptoRandomString({ length: 10 }), - revision: { - id: cryptoRandomString({ length: 10 }), - createdAt: new Date(), - publicKey, - userId: cryptoRandomString({ length: 10 }), - active: false, - triggers: [], - functions: [], - automationId: cryptoRandomString({ length: 10 }), - automationToken: cryptoRandomString({ length: 15 }) - } - }), - versionGetter: async () => undefined, - automationTokenGetter: async () => null - })({ - revisionId: cryptoRandomString({ length: 10 }), - manifest: { - triggerType: VersionCreationTriggerType, - modelId: cryptoRandomString({ length: 10 }), - versionId: cryptoRandomString({ length: 10 }) + active: false, + triggers: [], + functions: [], + automationId: cryptoRandomString({ length: 10 }), + automationToken: cryptoRandomString({ length: 15 }) } - }) - throw 'this should have thrown' - } catch (error) { - if (!(error instanceof Error)) throw error - expect(error.message).contains( - 'The automation is not enabled, cannot trigger it' - ) - } - }) - it('the revision is not active', async () => { - try { - await ensureRunConditions({ - revisionGetter: async () => ({ + }), + versionGetter: async () => undefined, + automationTokenGetter: async () => null + })({ + revisionId: cryptoRandomString({ length: 10 }), + manifest: { + triggerType: VersionCreationTriggerType, + modelId: cryptoRandomString({ length: 10 }), + versionId: cryptoRandomString({ length: 10 }) + } + }) + throw 'this should have thrown' + } catch (error) { + if (!(error instanceof Error)) throw error + expect(error.message).contains( + 'The automation is not enabled, cannot trigger it' + ) + } + }) + it('the revision is not active', async () => { + try { + await ensureRunConditions({ + revisionGetter: async () => ({ + id: cryptoRandomString({ length: 10 }), + name: cryptoRandomString({ length: 10 }), + projectId: cryptoRandomString({ length: 10 }), + enabled: true, + createdAt: new Date(), + updatedAt: new Date(), + executionEngineAutomationId: cryptoRandomString({ length: 10 }), + userId: cryptoRandomString({ length: 10 }), + revision: { + publicKey, + active: false, + triggers: [], + functions: [], + automationId: cryptoRandomString({ length: 10 }), + automationToken: cryptoRandomString({ length: 15 }), id: cryptoRandomString({ length: 10 }), - name: cryptoRandomString({ length: 10 }), - projectId: cryptoRandomString({ length: 10 }), - enabled: true, createdAt: new Date(), - updatedAt: new Date(), - executionEngineAutomationId: cryptoRandomString({ length: 10 }), - userId: cryptoRandomString({ length: 10 }), - revision: { - publicKey, - active: false, - triggers: [], - functions: [], - automationId: cryptoRandomString({ length: 10 }), - automationToken: cryptoRandomString({ length: 15 }), - id: cryptoRandomString({ length: 10 }), - createdAt: new Date(), - userId: cryptoRandomString({ length: 10 }) - } - }), - versionGetter: async () => undefined, - automationTokenGetter: async () => null - })({ - revisionId: cryptoRandomString({ length: 10 }), - manifest: { - triggerType: VersionCreationTriggerType, - modelId: cryptoRandomString({ length: 10 }), - versionId: cryptoRandomString({ length: 10 }) + userId: cryptoRandomString({ length: 10 }) } - }) - throw 'this should have thrown' - } catch (error) { - if (!(error instanceof Error)) throw error - expect(error.message).contains( - 'The automation revision is not active, cannot trigger it' - ) - } - }) - it("the revision doesn't have the referenced trigger", async () => { - try { - await ensureRunConditions({ - revisionGetter: async () => ({ + }), + versionGetter: async () => undefined, + automationTokenGetter: async () => null + })({ + revisionId: cryptoRandomString({ length: 10 }), + manifest: { + triggerType: VersionCreationTriggerType, + modelId: cryptoRandomString({ length: 10 }), + versionId: cryptoRandomString({ length: 10 }) + } + }) + throw 'this should have thrown' + } catch (error) { + if (!(error instanceof Error)) throw error + expect(error.message).contains( + 'The automation revision is not active, cannot trigger it' + ) + } + }) + it("the revision doesn't have the referenced trigger", async () => { + try { + await ensureRunConditions({ + revisionGetter: async () => ({ + id: cryptoRandomString({ length: 10 }), + createdAt: new Date(), + updatedAt: new Date(), + userId: cryptoRandomString({ length: 10 }), + name: cryptoRandomString({ length: 10 }), + projectId: cryptoRandomString({ length: 10 }), + enabled: true, + executionEngineAutomationId: cryptoRandomString({ length: 10 }), + revision: { + publicKey, id: cryptoRandomString({ length: 10 }), createdAt: new Date(), - updatedAt: new Date(), userId: cryptoRandomString({ length: 10 }), - name: cryptoRandomString({ length: 10 }), - projectId: cryptoRandomString({ length: 10 }), - enabled: true, - executionEngineAutomationId: cryptoRandomString({ length: 10 }), - revision: { - publicKey, - id: cryptoRandomString({ length: 10 }), - createdAt: new Date(), - userId: cryptoRandomString({ length: 10 }), - active: true, - triggers: [], - functions: [], - automationId: cryptoRandomString({ length: 10 }), - automationToken: cryptoRandomString({ length: 15 }) - } - }), - versionGetter: async () => undefined, - automationTokenGetter: async () => null - })({ - revisionId: cryptoRandomString({ length: 10 }), - manifest: { - triggerType: VersionCreationTriggerType, - modelId: cryptoRandomString({ length: 10 }), - versionId: cryptoRandomString({ length: 10 }) + active: true, + triggers: [], + functions: [], + automationId: cryptoRandomString({ length: 10 }), + automationToken: cryptoRandomString({ length: 15 }) } - }) - throw 'this should have thrown' - } catch (error) { - if (!(error instanceof Error)) throw error - expect(error.message).contains( - "The given revision doesn't have a trigger registered matching the input trigger" - ) - } - }) - it('the trigger is not a versionCreation type', async () => { - const manifest: VersionCreatedTriggerManifest = { - // @ts-expect-error: intentionally using invalid type here - triggerType: 'bogusTrigger' as const, - modelId: cryptoRandomString({ length: 10 }), - versionId: cryptoRandomString({ length: 10 }) - } + }), + versionGetter: async () => undefined, + automationTokenGetter: async () => null + })({ + revisionId: cryptoRandomString({ length: 10 }), + manifest: { + triggerType: VersionCreationTriggerType, + modelId: cryptoRandomString({ length: 10 }), + versionId: cryptoRandomString({ length: 10 }) + } + }) + throw 'this should have thrown' + } catch (error) { + if (!(error instanceof Error)) throw error + expect(error.message).contains( + "The given revision doesn't have a trigger registered matching the input trigger" + ) + } + }) + it('the trigger is not a versionCreation type', async () => { + const manifest: VersionCreatedTriggerManifest = { + // @ts-expect-error: intentionally using invalid type here + triggerType: 'bogusTrigger' as const, + modelId: cryptoRandomString({ length: 10 }), + versionId: cryptoRandomString({ length: 10 }) + } - try { - await ensureRunConditions({ - revisionGetter: async () => ({ + try { + await ensureRunConditions({ + revisionGetter: async () => ({ + id: cryptoRandomString({ length: 10 }), + name: cryptoRandomString({ length: 10 }), + projectId: cryptoRandomString({ length: 10 }), + enabled: true, + createdAt: new Date(), + updatedAt: new Date(), + executionEngineAutomationId: cryptoRandomString({ length: 10 }), + userId: cryptoRandomString({ length: 10 }), + revision: { + publicKey, id: cryptoRandomString({ length: 10 }), - name: cryptoRandomString({ length: 10 }), - projectId: cryptoRandomString({ length: 10 }), - enabled: true, createdAt: new Date(), - updatedAt: new Date(), - executionEngineAutomationId: cryptoRandomString({ length: 10 }), userId: cryptoRandomString({ length: 10 }), - revision: { - publicKey, - id: cryptoRandomString({ length: 10 }), - createdAt: new Date(), - userId: cryptoRandomString({ length: 10 }), - active: true, - triggers: [ - { - triggeringId: manifest.modelId, - triggerType: manifest.triggerType, - automationRevisionId: cryptoRandomString({ length: 10 }) - } - ], - functions: [], - automationId: cryptoRandomString({ length: 10 }) - } - }), - versionGetter: async () => undefined, - automationTokenGetter: async () => null - })({ - revisionId: cryptoRandomString({ length: 10 }), - manifest - }) - throw 'this should have thrown' - } catch (error) { - if (!(error instanceof Error)) throw error - expect(error.message).contains('Only model version triggers are supported') - } - }) - it("the version that is referenced on the trigger, doesn't exist", async () => { - const manifest: VersionCreatedTriggerManifest = { - triggerType: VersionCreationTriggerType, - modelId: cryptoRandomString({ length: 10 }), - versionId: cryptoRandomString({ length: 10 }), - projectId: cryptoRandomString({ length: 10 }) - } + active: true, + triggers: [ + { + triggeringId: manifest.modelId, + triggerType: manifest.triggerType, + automationRevisionId: cryptoRandomString({ length: 10 }) + } + ], + functions: [], + automationId: cryptoRandomString({ length: 10 }) + } + }), + versionGetter: async () => undefined, + automationTokenGetter: async () => null + })({ + revisionId: cryptoRandomString({ length: 10 }), + manifest + }) + throw 'this should have thrown' + } catch (error) { + if (!(error instanceof Error)) throw error + expect(error.message).contains('Only model version triggers are supported') + } + }) + it("the version that is referenced on the trigger, doesn't exist", async () => { + const manifest: VersionCreatedTriggerManifest = { + triggerType: VersionCreationTriggerType, + modelId: cryptoRandomString({ length: 10 }), + versionId: cryptoRandomString({ length: 10 }), + projectId: cryptoRandomString({ length: 10 }) + } - try { - await ensureRunConditions({ - revisionGetter: async () => ({ + try { + await ensureRunConditions({ + revisionGetter: async () => ({ + id: cryptoRandomString({ length: 10 }), + name: cryptoRandomString({ length: 10 }), + projectId: cryptoRandomString({ length: 10 }), + enabled: true, + createdAt: new Date(), + updatedAt: new Date(), + executionEngineAutomationId: cryptoRandomString({ length: 10 }), + userId: cryptoRandomString({ length: 10 }), + revision: { id: cryptoRandomString({ length: 10 }), - name: cryptoRandomString({ length: 10 }), - projectId: cryptoRandomString({ length: 10 }), - enabled: true, createdAt: new Date(), - updatedAt: new Date(), - executionEngineAutomationId: cryptoRandomString({ length: 10 }), userId: cryptoRandomString({ length: 10 }), - revision: { - id: cryptoRandomString({ length: 10 }), - createdAt: new Date(), - userId: cryptoRandomString({ length: 10 }), - active: true, - publicKey, - triggers: [ - { - triggerType: manifest.triggerType, - triggeringId: manifest.modelId, - automationRevisionId: cryptoRandomString({ length: 10 }) - } - ], - functions: [], - automationId: cryptoRandomString({ length: 10 }), - automationToken: cryptoRandomString({ length: 15 }) - } - }), - versionGetter: async () => undefined, - automationTokenGetter: async () => null - })({ - revisionId: cryptoRandomString({ length: 10 }), - manifest - }) - throw 'this should have thrown' - } catch (error) { - if (!(error instanceof Error)) throw error - expect(error.message).contains('The triggering version is not found') - } - }) - it("the author, that created the triggering version doesn't exist", async () => { - const manifest: VersionCreatedTriggerManifest = { - triggerType: VersionCreationTriggerType, - modelId: cryptoRandomString({ length: 10 }), - versionId: cryptoRandomString({ length: 10 }), - projectId: cryptoRandomString({ length: 10 }) - } + active: true, + publicKey, + triggers: [ + { + triggerType: manifest.triggerType, + triggeringId: manifest.modelId, + automationRevisionId: cryptoRandomString({ length: 10 }) + } + ], + functions: [], + automationId: cryptoRandomString({ length: 10 }), + automationToken: cryptoRandomString({ length: 15 }) + } + }), + versionGetter: async () => undefined, + automationTokenGetter: async () => null + })({ + revisionId: cryptoRandomString({ length: 10 }), + manifest + }) + throw 'this should have thrown' + } catch (error) { + if (!(error instanceof Error)) throw error + expect(error.message).contains('The triggering version is not found') + } + }) + it("the author, that created the triggering version doesn't exist", async () => { + const manifest: VersionCreatedTriggerManifest = { + triggerType: VersionCreationTriggerType, + modelId: cryptoRandomString({ length: 10 }), + versionId: cryptoRandomString({ length: 10 }), + projectId: cryptoRandomString({ length: 10 }) + } - try { - await ensureRunConditions({ - revisionGetter: async () => ({ + try { + await ensureRunConditions({ + revisionGetter: async () => ({ + id: cryptoRandomString({ length: 10 }), + name: cryptoRandomString({ length: 10 }), + projectId: cryptoRandomString({ length: 10 }), + createdAt: new Date(), + updatedAt: new Date(), + enabled: true, + executionEngineAutomationId: cryptoRandomString({ length: 10 }), + userId: cryptoRandomString({ length: 10 }), + revision: { id: cryptoRandomString({ length: 10 }), - name: cryptoRandomString({ length: 10 }), - projectId: cryptoRandomString({ length: 10 }), - createdAt: new Date(), - updatedAt: new Date(), - enabled: true, - executionEngineAutomationId: cryptoRandomString({ length: 10 }), userId: cryptoRandomString({ length: 10 }), - revision: { - id: cryptoRandomString({ length: 10 }), - userId: cryptoRandomString({ length: 10 }), - active: true, - publicKey, - triggers: [ - { - triggeringId: manifest.modelId, - triggerType: manifest.triggerType, - automationRevisionId: cryptoRandomString({ length: 10 }) - } - ], - createdAt: new Date(), - functions: [], - automationId: cryptoRandomString({ length: 10 }), - automationToken: cryptoRandomString({ length: 15 }) - } - }), - versionGetter: async () => ({ - author: null, - id: cryptoRandomString({ length: 10 }), + active: true, + publicKey, + triggers: [ + { + triggeringId: manifest.modelId, + triggerType: manifest.triggerType, + automationRevisionId: cryptoRandomString({ length: 10 }) + } + ], createdAt: new Date(), - message: 'foobar', - parents: [], - referencedObject: cryptoRandomString({ length: 10 }), - totalChildrenCount: null, - sourceApplication: 'test suite', - streamId: cryptoRandomString({ length: 10 }), - branchId: cryptoRandomString({ length: 10 }), - branchName: cryptoRandomString({ length: 10 }) - }), - automationTokenGetter: async () => null - })({ - revisionId: cryptoRandomString({ length: 10 }), - manifest - }) - throw 'this should have thrown' - } catch (error) { - if (!(error instanceof Error)) throw error - expect(error.message).contains( - "The user, that created the triggering version doesn't exist any more" - ) - } - }) - it("the automation doesn't have a token available", async () => { - const manifest: VersionCreatedTriggerManifest = { - triggerType: VersionCreationTriggerType, - modelId: cryptoRandomString({ length: 10 }), - versionId: cryptoRandomString({ length: 10 }), - projectId: cryptoRandomString({ length: 10 }) - } - try { - await ensureRunConditions({ - revisionGetter: async () => ({ + functions: [], + automationId: cryptoRandomString({ length: 10 }), + automationToken: cryptoRandomString({ length: 15 }) + } + }), + versionGetter: async () => ({ + author: null, + id: cryptoRandomString({ length: 10 }), + createdAt: new Date(), + message: 'foobar', + parents: [], + referencedObject: cryptoRandomString({ length: 10 }), + totalChildrenCount: null, + sourceApplication: 'test suite', + streamId: cryptoRandomString({ length: 10 }), + branchId: cryptoRandomString({ length: 10 }), + branchName: cryptoRandomString({ length: 10 }) + }), + automationTokenGetter: async () => null + })({ + revisionId: cryptoRandomString({ length: 10 }), + manifest + }) + throw 'this should have thrown' + } catch (error) { + if (!(error instanceof Error)) throw error + expect(error.message).contains( + "The user, that created the triggering version doesn't exist any more" + ) + } + }) + it("the automation doesn't have a token available", async () => { + const manifest: VersionCreatedTriggerManifest = { + triggerType: VersionCreationTriggerType, + modelId: cryptoRandomString({ length: 10 }), + versionId: cryptoRandomString({ length: 10 }), + projectId: cryptoRandomString({ length: 10 }) + } + try { + await ensureRunConditions({ + revisionGetter: async () => ({ + id: cryptoRandomString({ length: 10 }), + name: cryptoRandomString({ length: 10 }), + projectId: cryptoRandomString({ length: 10 }), + enabled: true, + createdAt: new Date(), + updatedAt: new Date(), + executionEngineAutomationId: cryptoRandomString({ length: 10 }), + userId: cryptoRandomString({ length: 10 }), + revision: { id: cryptoRandomString({ length: 10 }), - name: cryptoRandomString({ length: 10 }), - projectId: cryptoRandomString({ length: 10 }), - enabled: true, - createdAt: new Date(), - updatedAt: new Date(), - executionEngineAutomationId: cryptoRandomString({ length: 10 }), userId: cryptoRandomString({ length: 10 }), - revision: { - id: cryptoRandomString({ length: 10 }), - userId: cryptoRandomString({ length: 10 }), - createdAt: new Date(), - active: true, - publicKey, - triggers: [ - { - triggeringId: manifest.modelId, - triggerType: manifest.triggerType, - automationRevisionId: cryptoRandomString({ length: 10 }) - } - ], - functions: [], - automationId: cryptoRandomString({ length: 10 }), - automationToken: cryptoRandomString({ length: 15 }) - } - }), - versionGetter: async () => ({ - author: cryptoRandomString({ length: 10 }), - id: cryptoRandomString({ length: 10 }), createdAt: new Date(), - message: 'foobar', - parents: [], - referencedObject: cryptoRandomString({ length: 10 }), - totalChildrenCount: null, - sourceApplication: 'test suite', - streamId: cryptoRandomString({ length: 10 }), - branchId: cryptoRandomString({ length: 10 }), - branchName: cryptoRandomString({ length: 10 }) - }), - automationTokenGetter: async () => null - })({ - revisionId: cryptoRandomString({ length: 10 }), - manifest - }) - throw 'this should have thrown' - } catch (error) { - if (!(error instanceof Error)) throw error - expect(error.message).contains('Cannot find a token for the automation') - } - }) - }) - - describe('Run triggered manually', () => { - const buildManuallyTriggerAutomation = ( - overrides?: Partial - ) => { - const trigger = manuallyTriggerAutomation({ - getAutomationTriggerDefinitions, - getAutomation, - getBranchLatestCommits, - triggerFunction: triggerAutomationRevisionRun({ - automateRunTrigger: async () => ({ - automationRunId: cryptoRandomString({ length: 10 }) - }), - getFunctionInputDecryptor: getFunctionInputDecryptor({ buildDecryptor }), - getEncryptionKeyPairFor + active: true, + publicKey, + triggers: [ + { + triggeringId: manifest.modelId, + triggerType: manifest.triggerType, + automationRevisionId: cryptoRandomString({ length: 10 }) + } + ], + functions: [], + automationId: cryptoRandomString({ length: 10 }), + automationToken: cryptoRandomString({ length: 15 }) + } + }), + versionGetter: async () => ({ + author: cryptoRandomString({ length: 10 }), + id: cryptoRandomString({ length: 10 }), + createdAt: new Date(), + message: 'foobar', + parents: [], + referencedObject: cryptoRandomString({ length: 10 }), + totalChildrenCount: null, + sourceApplication: 'test suite', + streamId: cryptoRandomString({ length: 10 }), + branchId: cryptoRandomString({ length: 10 }), + branchName: cryptoRandomString({ length: 10 }) }), - ...(overrides || {}) + automationTokenGetter: async () => null + })({ + revisionId: cryptoRandomString({ length: 10 }), + manifest }) - return trigger + throw 'this should have thrown' + } catch (error) { + if (!(error instanceof Error)) throw error + expect(error.message).contains('Cannot find a token for the automation') } + }) + }) + + describe('Run triggered manually', () => { + const buildManuallyTriggerAutomation = ( + overrides?: Partial + ) => { + const trigger = manuallyTriggerAutomation({ + getAutomationTriggerDefinitions, + getAutomation, + getBranchLatestCommits, + triggerFunction: triggerAutomationRevisionRun({ + automateRunTrigger: async () => ({ + automationRunId: cryptoRandomString({ length: 10 }) + }), + getFunctionInputDecryptor: getFunctionInputDecryptor({ buildDecryptor }), + getEncryptionKeyPairFor + }), + ...(overrides || {}) + }) + return trigger + } - it('fails if referring to nonexistent automation', async () => { - const trigger = buildManuallyTriggerAutomation() + it('fails if referring to nonexistent automation', async () => { + const trigger = buildManuallyTriggerAutomation() - const e = await expectToThrow( - async () => - await trigger({ - automationId: cryptoRandomString({ length: 10 }), - userId: testUser.id, - projectId: testUserStream.id - }) - ) - expect(e.message).to.eq('Automation not found') - }) + const e = await expectToThrow( + async () => + await trigger({ + automationId: cryptoRandomString({ length: 10 }), + userId: testUser.id, + projectId: testUserStream.id + }) + ) + expect(e.message).to.eq('Automation not found') + }) - it('fails if project id is mismatched from automation id', async () => { - const trigger = buildManuallyTriggerAutomation() + it('fails if project id is mismatched from automation id', async () => { + const trigger = buildManuallyTriggerAutomation() - const e = await expectToThrow( - async () => - await trigger({ - automationId: createdAutomation.automation.id, - userId: testUser.id, - projectId: otherUserStream.id - }) - ) - expect(e.message).to.eq('Automation not found') + const e = await expectToThrow( + async () => + await trigger({ + automationId: createdAutomation.automation.id, + userId: testUser.id, + projectId: otherUserStream.id + }) + ) + expect(e.message).to.eq('Automation not found') + }) + + it('fails if revision has no version creation triggers', async () => { + const trigger = buildManuallyTriggerAutomation({ + getAutomationTriggerDefinitions: async () => [] }) - it('fails if revision has no version creation triggers', async () => { - const trigger = buildManuallyTriggerAutomation({ - getAutomationTriggerDefinitions: async () => [] + const e = await expectToThrow( + async () => + await trigger({ + automationId: createdAutomation.automation.id, + userId: testUser.id, + projectId: testUserStream.id + }) + ) + expect(e.message).to.eq( + 'No model version creation triggers found for the automation' + ) + }) + + it('fails if user does not have access to automation', async () => { + const trigger = buildManuallyTriggerAutomation() + + const e = await expectToThrow( + async () => + await trigger({ + automationId: createdAutomation.automation.id, + userId: otherUser.id, + projectId: testUserStream.id + }) + ) + expect(e.message).to.eq('User does not have required access to stream') + }) + + it('fails if no versions found for any triggers', async () => { + const trigger = buildManuallyTriggerAutomation() + + const e = await expectToThrow( + async () => + await trigger({ + automationId: createdAutomation.automation.id, + userId: testUser.id, + projectId: testUserStream.id + }) + ) + expect(e.message).to.eq( + 'No version to trigger on found for the available triggers' + ) + }) + + describe('with valid versions available', () => { + beforeEach(async () => { + await createTestCommit({ + id: '', + objectId: '', + streamId: testUserStream.id, + authorId: testUser.id }) + }) - const e = await expectToThrow( - async () => - await trigger({ - automationId: createdAutomation.automation.id, - userId: testUser.id, - projectId: testUserStream.id - }) - ) - expect(e.message).to.eq( - 'No model version creation triggers found for the automation' - ) + afterEach(async () => { + await truncateTables([Commits.name]) + await Promise.all([ + updateAutomation({ + id: createdAutomation.automation.id, + enabled: true + }), + updateAutomationRevision({ + id: createdRevision.id, + active: true + }) + ]) }) - it('fails if user does not have access to automation', async () => { + it('fails if automation is disabled', async () => { + await updateAutomation({ + id: createdAutomation.automation.id, + enabled: false + }) + const trigger = buildManuallyTriggerAutomation() const e = await expectToThrow( async () => await trigger({ automationId: createdAutomation.automation.id, - userId: otherUser.id, + userId: testUser.id, projectId: testUserStream.id }) ) - expect(e.message).to.eq('User does not have required access to stream') + expect(e.message).to.eq('The automation is not enabled, cannot trigger it') }) - it('fails if no versions found for any triggers', async () => { + it('fails if automation revision is disabled', async () => { + await updateAutomationRevision({ + id: createdRevision.id, + active: false + }) + const trigger = buildManuallyTriggerAutomation() const e = await expectToThrow( @@ -916,258 +979,199 @@ const { FF_AUTOMATE_MODULE_ENABLED } = Environment.getFeatureFlags() }) ) expect(e.message).to.eq( - 'No version to trigger on found for the available triggers' + 'No model version creation triggers found for the automation' ) }) - describe('with valid versions available', () => { - beforeEach(async () => { - await createTestCommit({ - id: '', - objectId: '', - streamId: testUserStream.id, - authorId: testUser.id - }) - }) + it('succeeds', async () => { + const trigger = buildManuallyTriggerAutomation() - afterEach(async () => { - await truncateTables([Commits.name]) - await Promise.all([ - updateAutomation({ - id: createdAutomation.automation.id, - enabled: true - }), - updateAutomationRevision({ - id: createdRevision.id, - active: true - }) - ]) + const { automationRunId } = await trigger({ + automationId: createdAutomation.automation.id, + userId: testUser.id, + projectId: testUserStream.id }) - it('fails if automation is disabled', async () => { - await updateAutomation({ - id: createdAutomation.automation.id, - enabled: false - }) + const storedRun = await getFullAutomationRunById(automationRunId) + expect(storedRun).to.be.ok - const trigger = buildManuallyTriggerAutomation() + const expectedStatus = 'pending' + expect(storedRun!.status).to.equal(expectedStatus) + for (const run of storedRun!.functionRuns) { + expect(run.status).to.equal(expectedStatus) + } + }) + }) + }) - const e = await expectToThrow( - async () => - await trigger({ - automationId: createdAutomation.automation.id, - userId: testUser.id, - projectId: testUserStream.id - }) - ) - expect(e.message).to.eq('The automation is not enabled, cannot trigger it') - }) + describe('Existing automation run', () => { + let automationRun: InsertableAutomationRun - it('fails if automation revision is disabled', async () => { - await updateAutomationRevision({ - id: createdRevision.id, - active: false - }) + before(async () => { + // Insert automation run directly to DB + automationRun = { + id: cryptoRandomString({ length: 10 }), + automationRevisionId: createdRevision.id, + createdAt: new Date(), + updatedAt: new Date(), + status: AutomationRunStatuses.running, + executionEngineRunId: cryptoRandomString({ length: 10 }), + triggers: [ + { + triggeringId: testUserStreamModel.id, + triggerType: VersionCreationTriggerType + } + ], + functionRuns: [ + { + functionId: generateFunctionId(), + functionReleaseId: generateFunctionReleaseId(), + id: cryptoRandomString({ length: 15 }), + status: AutomationRunStatuses.running, + elapsed: 0, + results: null, + contextView: null, + statusMessage: null, + createdAt: new Date(), + updatedAt: new Date() + } + ] + } - const trigger = buildManuallyTriggerAutomation() + await upsertAutomationRun(automationRun) + }) - const e = await expectToThrow( - async () => - await trigger({ - automationId: createdAutomation.automation.id, - userId: testUser.id, - projectId: testUserStream.id - }) - ) - expect(e.message).to.eq( - 'No model version creation triggers found for the automation' - ) + describe('status update report', () => { + const buildReportFunctionRunStatus = () => { + const report = reportFunctionRunStatus({ + getAutomationFunctionRunRecord: getFunctionRun, + upsertAutomationFunctionRunRecord: upsertAutomationFunctionRun, + automationRunUpdater: updateAutomationRun }) - it('succeeds', async () => { - const trigger = buildManuallyTriggerAutomation() - - const { automationRunId } = await trigger({ - automationId: createdAutomation.automation.id, - userId: testUser.id, - projectId: testUserStream.id - }) - - const storedRun = await getFullAutomationRunById(automationRunId) - expect(storedRun).to.be.ok + return report + } - const expectedStatus = 'pending' - expect(storedRun!.status).to.equal(expectedStatus) - for (const run of storedRun!.functionRuns) { - expect(run.status).to.equal(expectedStatus) - } - }) - }) - }) + it('fails fn with invalid functionRunId', async () => { + const report = buildReportFunctionRunStatus() - describe('Existing automation run', () => { - let automationRun: InsertableAutomationRun - - before(async () => { - // Insert automation run directly to DB - automationRun = { - id: cryptoRandomString({ length: 10 }), - automationRevisionId: createdRevision.id, - createdAt: new Date(), - updatedAt: new Date(), - status: AutomationRunStatuses.running, - executionEngineRunId: cryptoRandomString({ length: 10 }), - triggers: [ - { - triggeringId: testUserStreamModel.id, - triggerType: VersionCreationTriggerType - } - ], - functionRuns: [ - { - functionId: generateFunctionId(), - functionReleaseId: generateFunctionReleaseId(), - // runId: cryptoRandomString({ length: 15 }), - id: cryptoRandomString({ length: 15 }), - status: AutomationRunStatuses.running, - elapsed: 0, - results: null, - contextView: null, - statusMessage: null, - createdAt: new Date(), - updatedAt: new Date() - } - ] + const functionRunId = 'nonexistent' + const params: Parameters[0] = { + runId: functionRunId, + status: mapGqlStatusToDbStatus(AutomateRunStatus.Succeeded), + statusMessage: null, + results: null, + contextView: null } - await upsertAutomationRun(automationRun) + await expect(report(params)).to.eventually.be.rejectedWith( + FunctionRunNotFoundError + ) }) - describe('status update report', () => { - const buildReportFunctionRunStatus = () => { - const report = reportFunctionRunStatus({ - getAutomationFunctionRunRecord: getFunctionRun, - upsertAutomationFunctionRunRecord: upsertAutomationFunctionRun, - automationRunUpdater: updateAutomationRun - }) + it('fails fn with invalid status', async () => { + const report = buildReportFunctionRunStatus() - return report + const functionRunId = automationRun.functionRuns[0].id + const params: Parameters[0] = { + runId: functionRunId, + status: mapGqlStatusToDbStatus(AutomateRunStatus.Pending), + statusMessage: null, + results: null, + contextView: null } - it('fails fn with invalid functionRunId', async () => { - const report = buildReportFunctionRunStatus() - - const functionRunId = 'nonexistent' - const params: Parameters[0] = { - runId: functionRunId, - status: mapGqlStatusToDbStatus(AutomateRunStatus.Succeeded), - statusMessage: null, - results: null, - contextView: null + await expect(report(params)).to.eventually.be.rejectedWith( + FunctionRunReportStatusError, + /^Invalid status change/ + ) + }) + ;[ + { val: 1, error: 'invalid type' }, + { + val: { + version: '1.0', + values: { objectResults: [] }, + error: 'invalid version' } - - await expect(report(params)).to.eventually.be - .rejectedWith(FunctionRunNotFoundError) - }) - - it('fails fn with invalid status', async () => { + }, + { + val: { + version: 1.0, + values: {} + }, + error: 'invalid values object' + }, + { + val: { version: 1.0, values: { objectResults: [1] } }, + error: 'invalid objectResults item type' + }, + { + val: { version: 1.0, values: { objectResults: [{}] } }, + error: 'invalid objectResults item keys' + } + ].forEach(({ val, error }) => { + it('fails fn with invalid results: ' + error, async () => { const report = buildReportFunctionRunStatus() - const functionRunId = automationRun.functionRuns[0].runId + const functionRunId = automationRun.functionRuns[0].id const params: Parameters[0] = { runId: functionRunId, - status: mapGqlStatusToDbStatus(AutomateRunStatus.Pending), + status: mapGqlStatusToDbStatus(AutomateRunStatus.Succeeded), statusMessage: null, - results: null, + results: val as unknown as Automate.AutomateTypes.ResultsSchema, contextView: null } - await expect(report(params)).to.eventually.be - .rejectedWith(FunctionRunReportStatusError, /^Invalid status change/) + await expect(report(params)).to.eventually.be.rejectedWith( + Automate.UnformattableResultsSchemaError + ) }) - ;[ - { val: 1, error: 'invalid type' }, - { - val: { - version: '1.0', - values: { objectResults: [] }, - error: 'invalid version' - } - }, - { - val: { - version: 1.0, - values: {} - }, - error: 'invalid values object' - }, - { - val: { version: 1.0, values: { objectResults: [1] } }, - error: 'invalid objectResults item type' - }, - { - val: { version: 1.0, values: { objectResults: [{}] } }, - error: 'invalid objectResults item keys' - } - ].forEach(({ val, error }) => { - it('fails fn with invalid results: ' + error, async () => { - const report = buildReportFunctionRunStatus() - - const functionRunId = automationRun.functionRuns[0].id - const params: Parameters[0] = { - runId: functionRunId, - status: mapGqlStatusToDbStatus(AutomateRunStatus.Succeeded), - statusMessage: null, - results: val as unknown as Automate.AutomateTypes.ResultsSchema, - contextView: null - } - - await expect(report(params)).to.eventually.be - .rejectedWith(Automate.UnformattableResultsSchemaError, 'Invalid results schema') - }) - }) + }) - it('fails fn with invalid contextView url', async () => { - const report = buildReportFunctionRunStatus() + it('fails fn with invalid contextView url', async () => { + const report = buildReportFunctionRunStatus() - const functionRunId = automationRun.functionRuns[0].id - const params: Parameters[0] = { - runId: functionRunId, - status: mapGqlStatusToDbStatus(AutomateRunStatus.Succeeded), - statusMessage: null, - results: null, - contextView: 'invalid-url' - } - - await expect(report(params)).to.eventually.be - .rejectedWith(FunctionRunReportStatusError, 'Context view must start with a forward slash') - }) + const functionRunId = automationRun.functionRuns[0].id + const params: Parameters[0] = { + runId: functionRunId, + status: mapGqlStatusToDbStatus(AutomateRunStatus.Succeeded), + statusMessage: null, + results: null, + contextView: 'invalid-url' + } - it('succeeds', async () => { - const report = buildReportFunctionRunStatus() + await expect(report(params)).to.eventually.be.rejectedWith( + FunctionRunReportStatusError, + 'Context view must start with a forward slash' + ) + }) - const functionRunId = automationRun.functionRuns[0].id - const contextView = '/a/b/c' - const params: Parameters[0] = { - runId: functionRunId, - status: mapGqlStatusToDbStatus(AutomateRunStatus.Succeeded), - statusMessage: null, - results: null, - contextView - } + it('succeeds', async () => { + const report = buildReportFunctionRunStatus() + + const functionRunId = automationRun.functionRuns[0].id + const contextView = '/a/b/c' + const params: Parameters[0] = { + runId: functionRunId, + status: mapGqlStatusToDbStatus(AutomateRunStatus.Succeeded), + statusMessage: null, + results: null, + contextView + } - await expect(report(params)).to.eventually.be.true + await expect(report(params)).to.eventually.be.true - const [updatedRun, updatedFnRun] = await Promise.all([ - getFullAutomationRunById(automationRun.id), - getFunctionRun(functionRunId) - ]) + const [updatedRun, updatedFnRun] = await Promise.all([ + getFullAutomationRunById(automationRun.id), + getFunctionRun(functionRunId) + ]) - expect(updatedRun?.status).to.equal(AutomationRunStatuses.succeeded) - expect(updatedFnRun?.status).to.equal(AutomationRunStatuses.succeeded) - expect(updatedFnRun?.contextView).to.equal(contextView) - }) + expect(updatedRun?.status).to.equal(AutomationRunStatuses.succeeded) + expect(updatedFnRun?.status).to.equal(AutomationRunStatuses.succeeded) + expect(updatedFnRun?.contextView).to.equal(contextView) }) }) - } - ) + }) + } +)