a50e053096
- tidy up some passing of loggers to automate - do not use console.log, instead use @/observability/logging
1207 lines
39 KiB
TypeScript
1207 lines
39 KiB
TypeScript
import {
|
|
createFunction,
|
|
createFunctionWithoutVersion,
|
|
triggerAutomationRun,
|
|
updateFunction as execEngineUpdateFunction,
|
|
getFunctionFactory,
|
|
getFunctionReleaseFactory,
|
|
getPublicFunctionsFactory,
|
|
getFunctionReleasesFactory,
|
|
getUserGithubAuthState,
|
|
getUserGithubOrganizations,
|
|
getUserFunctionsFactory
|
|
} from '@/modules/automate/clients/executionEngine'
|
|
import {
|
|
GetProjectAutomationsParams,
|
|
getAutomationFactory,
|
|
getAutomationRunsItemsFactory,
|
|
getAutomationRunsTotalCountFactory,
|
|
getAutomationTokenFactory,
|
|
getAutomationTriggerDefinitionsFactory,
|
|
getFullAutomationRevisionMetadataFactory,
|
|
getFunctionRunFactory,
|
|
getLatestAutomationRevisionFactory,
|
|
getLatestVersionAutomationRunsFactory,
|
|
getProjectAutomationsItemsFactory,
|
|
getProjectAutomationsTotalCountFactory,
|
|
storeAutomationFactory,
|
|
storeAutomationRevisionFactory,
|
|
storeAutomationTokenFactory,
|
|
updateAutomationFactory,
|
|
updateAutomationRunFactory,
|
|
upsertAutomationFunctionRunFactory,
|
|
upsertAutomationRunFactory
|
|
} from '@/modules/automate/repositories/automations'
|
|
import {
|
|
createAutomationFactory,
|
|
createAutomationRevisionFactory,
|
|
createTestAutomationFactory,
|
|
getAutomationsStatusFactory,
|
|
validateAndUpdateAutomationFactory
|
|
} from '@/modules/automate/services/automationManagement'
|
|
import {
|
|
AuthCodePayloadAction,
|
|
createStoredAuthCodeFactory,
|
|
validateStoredAuthCodeFactory
|
|
} from '@/modules/automate/services/authCode'
|
|
import {
|
|
convertFunctionReleaseToGraphQLReturn,
|
|
convertFunctionToGraphQLReturn,
|
|
createFunctionFromTemplateFactory,
|
|
updateFunctionFactory
|
|
} from '@/modules/automate/services/functionManagement'
|
|
import {
|
|
Resolvers,
|
|
AutomateRunTriggerType
|
|
} from '@/modules/core/graph/generated/graphql'
|
|
import { getGenericRedis } from '@/modules/shared/redis/redis'
|
|
import { createAutomation as clientCreateAutomation } from '@/modules/automate/clients/executionEngine'
|
|
import {
|
|
Automate,
|
|
Roles,
|
|
isNullOrUndefined,
|
|
isNonNullable,
|
|
removeNullOrUndefinedKeys
|
|
} from '@speckle/shared'
|
|
import { getFeatureFlags, getServerOrigin } from '@/modules/shared/helpers/envHelper'
|
|
import {
|
|
getBranchesByIdsFactory,
|
|
getBranchLatestCommitsFactory
|
|
} from '@/modules/core/repositories/branches'
|
|
import {
|
|
createTestAutomationRunFactory,
|
|
manuallyTriggerAutomationFactory,
|
|
triggerAutomationRevisionRunFactory
|
|
} from '@/modules/automate/services/trigger'
|
|
import { reportFunctionRunStatusFactory } from '@/modules/automate/services/runsManagement'
|
|
import {
|
|
AutomationNotFoundError,
|
|
FunctionNotFoundError
|
|
} from '@/modules/automate/errors/management'
|
|
import {
|
|
FunctionReleaseSchemaType,
|
|
dbToGraphqlTriggerTypeMap,
|
|
functionTemplateRepos
|
|
} from '@/modules/automate/helpers/executionEngine'
|
|
import { authorizeResolver } from '@/modules/shared'
|
|
import {
|
|
AutomationRevisionFunctionForInputRedaction,
|
|
getEncryptionKeyPair,
|
|
getEncryptionKeyPairFor,
|
|
getEncryptionPublicKey,
|
|
getFunctionInputDecryptorFactory,
|
|
getFunctionInputsForFrontendFactory
|
|
} from '@/modules/automate/services/encryption'
|
|
import { buildDecryptor } from '@/modules/shared/utils/libsodium'
|
|
import { keyBy } from 'lodash'
|
|
import { redactWriteOnlyInputData } from '@/modules/automate/utils/jsonSchemaRedactor'
|
|
import {
|
|
ProjectSubscriptions,
|
|
filteredSubscribe
|
|
} from '@/modules/shared/utils/subscriptions'
|
|
import {
|
|
mapDbStatusToGqlStatus,
|
|
mapGqlStatusToDbStatus
|
|
} from '@/modules/automate/utils/automateFunctionRunStatus'
|
|
import { AutomateApiDisabledError } from '@/modules/automate/errors/core'
|
|
import {
|
|
ExecutionEngineFailedResponseError,
|
|
ExecutionEngineNetworkError
|
|
} from '@/modules/automate/errors/executionEngine'
|
|
import { db } from '@/db/knex'
|
|
import { getCommitFactory } from '@/modules/core/repositories/commits'
|
|
import { validateStreamAccessFactory } from '@/modules/core/services/streams/access'
|
|
import { getUserFactory } from '@/modules/core/repositories/users'
|
|
import { createAppTokenFactory } from '@/modules/core/services/tokens'
|
|
import {
|
|
storeApiTokenFactory,
|
|
storeTokenResourceAccessDefinitionsFactory,
|
|
storeTokenScopesFactory,
|
|
storeUserServerAppTokenFactory
|
|
} from '@/modules/core/repositories/tokens'
|
|
import { getEventBus } from '@/modules/shared/services/eventBus'
|
|
import { getProjectDbClient } from '@/modules/multiregion/utils/dbSelector'
|
|
import { BranchNotFoundError } from '@/modules/core/errors/branch'
|
|
import { withOperationLogging } from '@/observability/domain/businessLogging'
|
|
|
|
const { FF_AUTOMATE_MODULE_ENABLED } = getFeatureFlags()
|
|
|
|
const validateStreamAccess = validateStreamAccessFactory({ authorizeResolver })
|
|
const createAppToken = createAppTokenFactory({
|
|
storeApiToken: storeApiTokenFactory({ db }),
|
|
storeTokenScopes: storeTokenScopesFactory({ db }),
|
|
storeTokenResourceAccessDefinitions: storeTokenResourceAccessDefinitionsFactory({
|
|
db
|
|
}),
|
|
storeUserServerAppToken: storeUserServerAppTokenFactory({ db })
|
|
})
|
|
|
|
export = (FF_AUTOMATE_MODULE_ENABLED
|
|
? {
|
|
/**
|
|
* If automate module is enabled
|
|
*/
|
|
AutomationRevisionTriggerDefinition: {
|
|
__resolveType(parent) {
|
|
if (
|
|
dbToGraphqlTriggerTypeMap[parent.triggerType] ===
|
|
AutomateRunTriggerType.VersionCreated
|
|
) {
|
|
return 'VersionCreatedTriggerDefinition'
|
|
}
|
|
return null
|
|
}
|
|
},
|
|
AutomationRunTrigger: {
|
|
__resolveType(parent) {
|
|
if (
|
|
dbToGraphqlTriggerTypeMap[parent.triggerType] ===
|
|
AutomateRunTriggerType.VersionCreated
|
|
) {
|
|
return 'VersionCreatedTrigger'
|
|
}
|
|
return null
|
|
}
|
|
},
|
|
VersionCreatedTriggerDefinition: {
|
|
type: () => AutomateRunTriggerType.VersionCreated,
|
|
async model(parent, _args, ctx) {
|
|
const projectDb = await getProjectDbClient({ projectId: parent.projectId })
|
|
return ctx.loaders
|
|
.forRegion({ db: projectDb })
|
|
.branches.getById.load(parent.triggeringId)
|
|
}
|
|
},
|
|
VersionCreatedTrigger: {
|
|
type: () => AutomateRunTriggerType.VersionCreated,
|
|
async version(parent, _args, ctx) {
|
|
const projectDb = await getProjectDbClient({ projectId: parent.projectId })
|
|
return ctx.loaders
|
|
.forRegion({ db: projectDb })
|
|
.commits.getById.load(parent.triggeringId)
|
|
},
|
|
async model(parent, _args, ctx) {
|
|
const projectDb = await getProjectDbClient({ projectId: parent.projectId })
|
|
return ctx.loaders
|
|
.forRegion({ db: projectDb })
|
|
.commits.getCommitBranch.load(parent.triggeringId)
|
|
}
|
|
},
|
|
ProjectTriggeredAutomationsStatusUpdatedMessage: {
|
|
async project(parent, _args, ctx) {
|
|
const projectDb = await getProjectDbClient({ projectId: parent.projectId })
|
|
return ctx.loaders
|
|
.forRegion({ db: projectDb })
|
|
.streams.getStream.load(parent.projectId)
|
|
},
|
|
async model(parent, _args, ctx) {
|
|
const projectDb = await getProjectDbClient({ projectId: parent.projectId })
|
|
return ctx.loaders
|
|
.forRegion({ db: projectDb })
|
|
.branches.getById.load(parent.modelId)
|
|
},
|
|
async version(parent, _args, ctx) {
|
|
const projectDb = await getProjectDbClient({ projectId: parent.projectId })
|
|
return ctx.loaders
|
|
.forRegion({ db: projectDb })
|
|
.commits.getById.load(parent.versionId)
|
|
}
|
|
},
|
|
Project: {
|
|
async automation(parent, args, ctx) {
|
|
const projectDb = await getProjectDbClient({ projectId: parent.id })
|
|
|
|
const res = ctx.loaders
|
|
.forRegion({ db: projectDb })
|
|
.streams.getAutomation.forStream(parent.id)
|
|
.load(args.id)
|
|
|
|
if (!res) {
|
|
if (!res) {
|
|
throw new AutomationNotFoundError()
|
|
}
|
|
}
|
|
|
|
return res
|
|
},
|
|
async automations(parent, args) {
|
|
const projectDb = await getProjectDbClient({ projectId: parent.id })
|
|
|
|
const retrievalArgs: GetProjectAutomationsParams = {
|
|
projectId: parent.id,
|
|
args
|
|
}
|
|
|
|
const [{ items, cursor }, totalCount] = await Promise.all([
|
|
getProjectAutomationsItemsFactory({ db: projectDb })(retrievalArgs),
|
|
getProjectAutomationsTotalCountFactory({ db: projectDb })(retrievalArgs)
|
|
])
|
|
|
|
return {
|
|
items,
|
|
totalCount,
|
|
cursor
|
|
}
|
|
}
|
|
},
|
|
Model: {
|
|
async automationsStatus(parent, _args, ctx) {
|
|
const projectDb = await getProjectDbClient({ projectId: parent.streamId })
|
|
|
|
const getLatestVersionAutomationRuns = getLatestVersionAutomationRunsFactory({
|
|
db: projectDb
|
|
})
|
|
|
|
const getStatus = getAutomationsStatusFactory({
|
|
getLatestVersionAutomationRuns
|
|
})
|
|
|
|
const modelId = parent.id
|
|
const projectId = parent.streamId
|
|
const latestCommit = await ctx.loaders
|
|
.forRegion({ db: projectDb })
|
|
.branches.getLatestCommit.load(parent.id)
|
|
|
|
// if the model has no versions, no automations could have run
|
|
if (!latestCommit) return null
|
|
|
|
return await getStatus({
|
|
projectId,
|
|
modelId,
|
|
versionId: latestCommit.id
|
|
})
|
|
}
|
|
},
|
|
Version: {
|
|
async automationsStatus(parent, _args, ctx) {
|
|
const projectDb = await getProjectDbClient({ projectId: parent.streamId })
|
|
|
|
const getStatus = getAutomationsStatusFactory({
|
|
getLatestVersionAutomationRuns: getLatestVersionAutomationRunsFactory({
|
|
db: projectDb
|
|
})
|
|
})
|
|
|
|
const versionId = parent.id
|
|
const branch = await ctx.loaders
|
|
.forRegion({ db: projectDb })
|
|
.commits.getCommitBranch.load(versionId)
|
|
if (!branch) throw new BranchNotFoundError('Invalid version Id')
|
|
|
|
const projectId = branch.streamId
|
|
const modelId = branch.id
|
|
return await getStatus({
|
|
projectId,
|
|
modelId,
|
|
versionId
|
|
})
|
|
}
|
|
},
|
|
Automation: {
|
|
async currentRevision(parent, _args, ctx) {
|
|
const projectDb = await getProjectDbClient({ projectId: parent.projectId })
|
|
const automationRevision = await ctx.loaders
|
|
.forRegion({ db: projectDb })
|
|
.automations.getLatestAutomationRevision.load(parent.id)
|
|
return { ...automationRevision, projectId: parent.projectId }
|
|
},
|
|
async runs(parent, args) {
|
|
const projectDb = await getProjectDbClient({ projectId: parent.projectId })
|
|
|
|
const retrievalArgs = {
|
|
automationId: parent.id,
|
|
...args
|
|
}
|
|
|
|
const [{ items, cursor }, totalCount] = await Promise.all([
|
|
getAutomationRunsItemsFactory({ db: projectDb })({
|
|
args: retrievalArgs
|
|
}),
|
|
getAutomationRunsTotalCountFactory({ db: projectDb })({
|
|
args: retrievalArgs
|
|
})
|
|
])
|
|
|
|
return {
|
|
items,
|
|
totalCount,
|
|
cursor
|
|
}
|
|
},
|
|
async creationPublicKeys(parent, _args, ctx) {
|
|
await authorizeResolver(
|
|
ctx.userId,
|
|
parent.projectId,
|
|
Roles.Stream.Contributor,
|
|
ctx.resourceAccessRules
|
|
)
|
|
|
|
const publicKey = await getEncryptionPublicKey()
|
|
return [publicKey]
|
|
}
|
|
},
|
|
AutomateRun: {
|
|
async trigger(parent, _args, ctx) {
|
|
const projectDb = await getProjectDbClient({ projectId: parent.projectId })
|
|
|
|
const triggers =
|
|
parent.triggers ||
|
|
(await ctx.loaders
|
|
.forRegion({ db: projectDb })
|
|
.automations.getRunTriggers.load(parent.id))
|
|
|
|
const trigger = triggers[0]
|
|
return { ...trigger, projectId: parent.projectId }
|
|
},
|
|
async functionRuns(parent) {
|
|
return parent.functionRuns
|
|
},
|
|
async automation(parent, _args, ctx) {
|
|
const projectDb = await getProjectDbClient({ projectId: parent.projectId })
|
|
|
|
return ctx.loaders
|
|
.forRegion({ db: projectDb })
|
|
.automations.getAutomation.load(parent.automationId)
|
|
},
|
|
status: (parent) => mapDbStatusToGqlStatus(parent.status)
|
|
},
|
|
TriggeredAutomationsStatus: {
|
|
status: (parent) => mapDbStatusToGqlStatus(parent.status)
|
|
},
|
|
AutomateFunctionRun: {
|
|
async function(parent, _args, ctx) {
|
|
const fn = await ctx.loaders.automationsApi.getFunction.load(
|
|
parent.functionId
|
|
)
|
|
if (!fn) {
|
|
ctx.log.warn(
|
|
{ id: parent.functionId, fnRunId: parent.id, runid: parent.runId },
|
|
'AutomateFunctionRun function unexpectedly not found'
|
|
)
|
|
return null
|
|
}
|
|
|
|
return convertFunctionToGraphQLReturn(fn)
|
|
},
|
|
results(parent, _args, ctx) {
|
|
try {
|
|
return parent.results
|
|
? Automate.AutomateTypes.formatResultsSchema(parent.results)
|
|
: null
|
|
} catch (e) {
|
|
ctx.log.warn('Error formatting results schema', e)
|
|
}
|
|
},
|
|
status: (parent) => mapDbStatusToGqlStatus(parent.status)
|
|
},
|
|
AutomationRevision: {
|
|
async triggerDefinitions(parent, _args, ctx) {
|
|
const projectDb = await getProjectDbClient({ projectId: parent.projectId })
|
|
|
|
const triggers = await ctx.loaders
|
|
.forRegion({ db: projectDb })
|
|
.automations.getRevisionTriggerDefinitions.load(parent.id)
|
|
|
|
return triggers.map((trigger) => ({
|
|
...trigger,
|
|
projectId: parent.projectId
|
|
}))
|
|
},
|
|
async functions(parent, _args, ctx) {
|
|
const projectDb = await getProjectDbClient({ projectId: parent.projectId })
|
|
|
|
const prepareInputs = getFunctionInputsForFrontendFactory({
|
|
getEncryptionKeyPairFor,
|
|
buildDecryptor,
|
|
redactWriteOnlyInputData
|
|
})
|
|
|
|
const fns = await ctx.loaders
|
|
.forRegion({ db: projectDb })
|
|
.automations.getRevisionFunctions.load(parent.id)
|
|
const fnsReleases = keyBy(
|
|
(
|
|
await ctx.loaders
|
|
.forRegion({ db: projectDb })
|
|
.automationsApi.getFunctionRelease.loadMany(
|
|
fns.map((fn) => [fn.functionId, fn.functionReleaseId])
|
|
)
|
|
).filter(
|
|
(r): r is FunctionReleaseSchemaType => r !== null && !(r instanceof Error)
|
|
),
|
|
(r) => r.functionVersionId
|
|
)
|
|
|
|
const fnsForRedaction: Array<AutomationRevisionFunctionForInputRedaction | null> =
|
|
fns.map((fn) => {
|
|
const release = fnsReleases[fn.functionReleaseId]
|
|
if (!release) {
|
|
return null
|
|
}
|
|
|
|
return {
|
|
...fn,
|
|
release
|
|
}
|
|
})
|
|
|
|
return prepareInputs({
|
|
fns: fnsForRedaction.filter(isNonNullable),
|
|
publicKey: parent.publicKey
|
|
})
|
|
}
|
|
},
|
|
AutomationRevisionFunction: {
|
|
async parameters(parent) {
|
|
return parent.functionInputs
|
|
}
|
|
},
|
|
AutomateFunction: {
|
|
async releases(parent, args, context) {
|
|
try {
|
|
// TODO: Replace w/ dataloader batch call, when/if possible
|
|
const fn = await getFunctionFactory({
|
|
logger: context.log
|
|
})({
|
|
functionId: parent.id,
|
|
releases:
|
|
args?.cursor || args?.filter?.search || args?.limit
|
|
? {
|
|
cursor: args.cursor || undefined,
|
|
versionsFilter: args.filter?.search || undefined,
|
|
limit: args.limit || undefined
|
|
}
|
|
: {}
|
|
})
|
|
|
|
if (!fn) {
|
|
return {
|
|
cursor: null,
|
|
totalCount: 0,
|
|
items: []
|
|
}
|
|
}
|
|
|
|
return {
|
|
cursor: fn.versionCursor,
|
|
totalCount: fn.versionCount,
|
|
items: fn.functionVersions.map((r) =>
|
|
convertFunctionReleaseToGraphQLReturn({ ...r, functionId: parent.id })
|
|
)
|
|
}
|
|
} catch (e) {
|
|
const isNotFound =
|
|
e instanceof ExecutionEngineFailedResponseError &&
|
|
e.response.statusMessage === 'FunctionNotFound'
|
|
if (e instanceof ExecutionEngineNetworkError || isNotFound) {
|
|
return {
|
|
cursor: null,
|
|
totalCount: 0,
|
|
items: []
|
|
}
|
|
}
|
|
|
|
throw e
|
|
}
|
|
},
|
|
async creator(parent, _args, ctx) {
|
|
if (
|
|
!parent.functionCreator ||
|
|
parent.functionCreator.speckleServerOrigin !== getServerOrigin()
|
|
) {
|
|
return null
|
|
}
|
|
|
|
return ctx.loaders.users.getUser.load(parent.functionCreator.speckleUserId)
|
|
}
|
|
},
|
|
AutomateFunctionRelease: {
|
|
async function(parent, _args, ctx) {
|
|
const fn = await ctx.loaders.automationsApi.getFunction.load(
|
|
parent.functionId
|
|
)
|
|
if (!fn) {
|
|
throw new FunctionNotFoundError('Function not found', {
|
|
info: { id: parent.functionId }
|
|
})
|
|
}
|
|
|
|
return convertFunctionToGraphQLReturn(fn)
|
|
}
|
|
},
|
|
AutomateMutations: {
|
|
async createFunction(_parent, args, ctx) {
|
|
const logger = ctx.log
|
|
const create = createFunctionFromTemplateFactory({
|
|
createExecutionEngineFn: createFunction,
|
|
getUser: getUserFactory({ db }),
|
|
createStoredAuthCode: createStoredAuthCodeFactory({
|
|
redis: getGenericRedis()
|
|
}),
|
|
logger
|
|
})
|
|
|
|
const { graphqlReturn } = await withOperationLogging(
|
|
async () => await create({ input: args.input, userId: ctx.userId! }),
|
|
{
|
|
logger,
|
|
operationName: 'createFunction',
|
|
operationDescription: 'Create a new Automate function'
|
|
}
|
|
)
|
|
return graphqlReturn
|
|
},
|
|
async createFunctionWithoutVersion(_parent, args, ctx) {
|
|
const logger = ctx.log
|
|
|
|
const authCode = await createStoredAuthCodeFactory({
|
|
redis: getGenericRedis()
|
|
})({
|
|
userId: ctx.userId!,
|
|
action: AuthCodePayloadAction.CreateFunction
|
|
})
|
|
return await withOperationLogging(
|
|
async () =>
|
|
await createFunctionWithoutVersion({
|
|
body: {
|
|
speckleServerAuthenticationPayload: {
|
|
...authCode,
|
|
origin: getServerOrigin()
|
|
},
|
|
functionName: args.input.name,
|
|
description: args.input.description,
|
|
repositoryUrl:
|
|
'https://github.com/specklesystems/speckle_automate_python_example',
|
|
supportedSourceApps: [],
|
|
tags: []
|
|
}
|
|
}),
|
|
{
|
|
logger,
|
|
operationName: 'createFunctionWithoutVersion',
|
|
operationDescription: 'Create a new Automate function without version'
|
|
}
|
|
)
|
|
},
|
|
async updateFunction(_parent, args, ctx) {
|
|
const functionId = args.input.id
|
|
const logger = ctx.log.child({
|
|
functionId
|
|
})
|
|
const update = updateFunctionFactory({
|
|
updateFunction: execEngineUpdateFunction,
|
|
getFunction: getFunctionFactory({ logger }),
|
|
createStoredAuthCode: createStoredAuthCodeFactory({
|
|
redis: getGenericRedis()
|
|
})
|
|
})
|
|
return await withOperationLogging(
|
|
async () => await update({ input: args.input, userId: ctx.userId! }),
|
|
{
|
|
logger,
|
|
operationName: 'updateFunction',
|
|
operationDescription: 'Update an Automate function'
|
|
}
|
|
)
|
|
}
|
|
},
|
|
ProjectAutomationMutations: {
|
|
async create(parent, { input }, ctx) {
|
|
const projectId = parent.projectId
|
|
|
|
const logger = ctx.log.child({
|
|
projectId,
|
|
streamId: projectId //legacy
|
|
})
|
|
|
|
const projectDb = await getProjectDbClient({ projectId })
|
|
|
|
const create = createAutomationFactory({
|
|
createAuthCode: createStoredAuthCodeFactory({ redis: getGenericRedis() }),
|
|
automateCreateAutomation: clientCreateAutomation,
|
|
storeAutomation: storeAutomationFactory({ db: projectDb }),
|
|
storeAutomationToken: storeAutomationTokenFactory({ db: projectDb }),
|
|
validateStreamAccess,
|
|
eventEmit: getEventBus().emit
|
|
})
|
|
|
|
const { automation } = await withOperationLogging(
|
|
async () =>
|
|
await create({
|
|
input,
|
|
userId: ctx.userId!,
|
|
projectId,
|
|
userResourceAccessRules: ctx.resourceAccessRules
|
|
}),
|
|
{
|
|
logger,
|
|
operationName: 'createProjectAutomation',
|
|
operationDescription: 'Create a new Automation attached to a project'
|
|
}
|
|
)
|
|
|
|
return automation
|
|
},
|
|
async update(parent, { input }, ctx) {
|
|
const projectId = parent.projectId
|
|
const automationId = input.id
|
|
|
|
const logger = ctx.log.child({
|
|
projectId,
|
|
streamId: projectId, //legacy
|
|
automationId
|
|
})
|
|
|
|
const projectDb = await getProjectDbClient({ projectId })
|
|
|
|
const update = validateAndUpdateAutomationFactory({
|
|
getAutomation: getAutomationFactory({ db: projectDb }),
|
|
updateAutomation: updateAutomationFactory({ db: projectDb }),
|
|
validateStreamAccess,
|
|
eventEmit: getEventBus().emit
|
|
})
|
|
|
|
return await withOperationLogging(
|
|
async () =>
|
|
await update({
|
|
input,
|
|
userId: ctx.userId!,
|
|
projectId,
|
|
userResourceAccessRules: ctx.resourceAccessRules
|
|
}),
|
|
{
|
|
logger,
|
|
operationName: 'updateProjectAutomation',
|
|
operationDescription: 'Update an Automation attached to a project'
|
|
}
|
|
)
|
|
},
|
|
async createRevision(parent, { input }, ctx) {
|
|
const projectId = parent.projectId
|
|
const automationId = input.automationId
|
|
|
|
const logger = ctx.log.child({
|
|
projectId,
|
|
streamId: projectId, //legacy
|
|
automationId
|
|
})
|
|
|
|
const projectDb = await getProjectDbClient({ projectId })
|
|
|
|
const create = createAutomationRevisionFactory({
|
|
getAutomation: getAutomationFactory({ db: projectDb }),
|
|
storeAutomationRevision: storeAutomationRevisionFactory({ db: projectDb }),
|
|
getBranchesByIds: getBranchesByIdsFactory({ db: projectDb }),
|
|
getFunctionRelease: getFunctionReleaseFactory({ logger: ctx.log }),
|
|
getEncryptionKeyPair,
|
|
getFunctionInputDecryptor: getFunctionInputDecryptorFactory({
|
|
buildDecryptor
|
|
}),
|
|
getFunctionReleases: getFunctionReleasesFactory({ logger: ctx.log }),
|
|
eventEmit: getEventBus().emit,
|
|
validateStreamAccess
|
|
})
|
|
|
|
return await withOperationLogging(
|
|
async () =>
|
|
await create({
|
|
input,
|
|
projectId,
|
|
userId: ctx.userId!,
|
|
userResourceAccessRules: ctx.resourceAccessRules
|
|
}),
|
|
{
|
|
logger,
|
|
operationName: 'createAutomationRevision',
|
|
operationDescription: 'Create a new Automation revision'
|
|
}
|
|
)
|
|
},
|
|
async trigger(parent, { automationId }, ctx) {
|
|
const projectId = parent.projectId
|
|
|
|
const logger = ctx.log.child({
|
|
projectId,
|
|
streamId: projectId, //legacy
|
|
automationId
|
|
})
|
|
|
|
const projectDb = await getProjectDbClient({ projectId })
|
|
|
|
const trigger = manuallyTriggerAutomationFactory({
|
|
getAutomationTriggerDefinitions: getAutomationTriggerDefinitionsFactory({
|
|
db: projectDb
|
|
}),
|
|
getAutomation: getAutomationFactory({ db: projectDb }),
|
|
getBranchLatestCommits: getBranchLatestCommitsFactory({ db: projectDb }),
|
|
triggerFunction: triggerAutomationRevisionRunFactory({
|
|
automateRunTrigger: triggerAutomationRun,
|
|
getEncryptionKeyPairFor,
|
|
getFunctionInputDecryptor: getFunctionInputDecryptorFactory({
|
|
buildDecryptor
|
|
}),
|
|
createAppToken,
|
|
emitEvent: getEventBus().emit,
|
|
getAutomationToken: getAutomationTokenFactory({ db: projectDb }),
|
|
upsertAutomationRun: upsertAutomationRunFactory({ db: projectDb }),
|
|
getFullAutomationRevisionMetadata:
|
|
getFullAutomationRevisionMetadataFactory({ db: projectDb }),
|
|
getBranchLatestCommits: getBranchLatestCommitsFactory({ db: projectDb }),
|
|
getCommit: getCommitFactory({ db: projectDb })
|
|
}),
|
|
validateStreamAccess
|
|
})
|
|
|
|
const { automationRunId } = await withOperationLogging(
|
|
async () =>
|
|
await trigger({
|
|
automationId,
|
|
userId: ctx.userId!,
|
|
userResourceAccessRules: ctx.resourceAccessRules,
|
|
projectId
|
|
}),
|
|
{
|
|
logger,
|
|
operationName: 'triggerProjectAutomation',
|
|
operationDescription: 'Trigger an Automation'
|
|
}
|
|
)
|
|
|
|
return automationRunId
|
|
},
|
|
async createTestAutomation(parent, { input }, ctx) {
|
|
const projectId = parent.projectId
|
|
|
|
const logger = ctx.log.child({
|
|
projectId,
|
|
streamId: projectId //legacy
|
|
})
|
|
|
|
const projectDb = await getProjectDbClient({ projectId })
|
|
|
|
const create = createTestAutomationFactory({
|
|
getEncryptionKeyPair,
|
|
getFunction: getFunctionFactory({ logger: ctx.log }),
|
|
storeAutomation: storeAutomationFactory({ db: projectDb }),
|
|
storeAutomationRevision: storeAutomationRevisionFactory({ db: projectDb }),
|
|
validateStreamAccess,
|
|
eventEmit: getEventBus().emit
|
|
})
|
|
|
|
return await withOperationLogging(
|
|
async () =>
|
|
await create({
|
|
input,
|
|
projectId,
|
|
userId: ctx.userId!,
|
|
userResourceAccessRules: ctx.resourceAccessRules
|
|
}),
|
|
{
|
|
logger,
|
|
operationName: 'createTestAutomation',
|
|
operationDescription: 'Create a new test Automation'
|
|
}
|
|
)
|
|
},
|
|
async createTestAutomationRun(parent, { automationId }, ctx) {
|
|
const projectId = parent.projectId
|
|
|
|
const logger = ctx.log.child({
|
|
projectId,
|
|
streamId: projectId, //legacy
|
|
automationId
|
|
})
|
|
|
|
const projectDb = await getProjectDbClient({ projectId: parent.projectId })
|
|
|
|
const create = createTestAutomationRunFactory({
|
|
getEncryptionKeyPairFor,
|
|
getFunctionInputDecryptor: getFunctionInputDecryptorFactory({
|
|
buildDecryptor
|
|
}),
|
|
getAutomation: getAutomationFactory({
|
|
db: projectDb
|
|
}),
|
|
getLatestAutomationRevision: getLatestAutomationRevisionFactory({
|
|
db: projectDb
|
|
}),
|
|
getFullAutomationRevisionMetadata: getFullAutomationRevisionMetadataFactory(
|
|
{
|
|
db: projectDb
|
|
}
|
|
),
|
|
upsertAutomationRun: upsertAutomationRunFactory({
|
|
db: projectDb
|
|
}),
|
|
getBranchLatestCommits: getBranchLatestCommitsFactory({
|
|
db: projectDb
|
|
}),
|
|
emitEvent: getEventBus().emit,
|
|
validateStreamAccess
|
|
})
|
|
|
|
return await withOperationLogging(
|
|
async () =>
|
|
await create({
|
|
projectId: parent.projectId,
|
|
automationId,
|
|
userId: ctx.userId!
|
|
}),
|
|
{
|
|
logger,
|
|
operationName: 'createTestAutomationRun',
|
|
operationDescription: 'Create a new test Automation run'
|
|
}
|
|
)
|
|
}
|
|
},
|
|
Query: {
|
|
async automateValidateAuthCode(_parent, args, ctx) {
|
|
const validate = validateStoredAuthCodeFactory({
|
|
redis: getGenericRedis(),
|
|
emit: getEventBus().emit,
|
|
logger: ctx.log
|
|
})
|
|
const payload = removeNullOrUndefinedKeys(args.payload)
|
|
const resources = removeNullOrUndefinedKeys(args.resources ?? {})
|
|
return await validate({
|
|
payload: {
|
|
...payload,
|
|
action: args.payload.action as AuthCodePayloadAction
|
|
},
|
|
resources
|
|
})
|
|
},
|
|
async automateFunction(_parent, { id }, ctx) {
|
|
const fn = await ctx.loaders.automationsApi.getFunction.load(id)
|
|
if (!fn) {
|
|
throw new FunctionNotFoundError('Function not found', {
|
|
info: { id }
|
|
})
|
|
}
|
|
|
|
return convertFunctionToGraphQLReturn(fn)
|
|
},
|
|
async automateFunctions(_parent, args, ctx) {
|
|
try {
|
|
const res = await getPublicFunctionsFactory({
|
|
logger: ctx.log
|
|
})({
|
|
query: {
|
|
query: args.filter?.search || undefined,
|
|
cursor: args.cursor || undefined,
|
|
limit: isNullOrUndefined(args.limit) ? undefined : args.limit,
|
|
functionsWithoutVersions:
|
|
args.filter?.functionsWithoutReleases || undefined
|
|
}
|
|
})
|
|
|
|
if (!res) {
|
|
return {
|
|
cursor: null,
|
|
totalCount: 0,
|
|
items: []
|
|
}
|
|
}
|
|
|
|
const items = res.items.map(convertFunctionToGraphQLReturn)
|
|
|
|
return {
|
|
cursor: res.cursor,
|
|
totalCount: res.totalCount,
|
|
items
|
|
}
|
|
} catch (e) {
|
|
const isNotFound =
|
|
e instanceof ExecutionEngineFailedResponseError &&
|
|
e.response.statusMessage === 'FunctionNotFound'
|
|
if (e instanceof ExecutionEngineNetworkError || isNotFound) {
|
|
return {
|
|
cursor: null,
|
|
totalCount: 0,
|
|
items: []
|
|
}
|
|
}
|
|
|
|
throw e
|
|
}
|
|
}
|
|
},
|
|
User: {
|
|
automateInfo: (parent) => ({ userId: parent.id }),
|
|
automateFunctions: async (_parent, args, context) => {
|
|
try {
|
|
const authCode = await createStoredAuthCodeFactory({
|
|
redis: getGenericRedis()
|
|
})({
|
|
userId: context.userId!,
|
|
action: AuthCodePayloadAction.ListUserFunctions
|
|
})
|
|
|
|
const res = await getUserFunctionsFactory({
|
|
logger: context.log
|
|
})({
|
|
userId: context.userId!,
|
|
query: {
|
|
query: args.filter?.search || undefined,
|
|
cursor: args.cursor || undefined,
|
|
limit: isNullOrUndefined(args.limit) ? undefined : args.limit
|
|
},
|
|
body: {
|
|
speckleServerAuthenticationPayload: {
|
|
...authCode,
|
|
origin: getServerOrigin()
|
|
}
|
|
}
|
|
})
|
|
|
|
if (!res) {
|
|
return {
|
|
cursor: null,
|
|
totalCount: 0,
|
|
items: []
|
|
}
|
|
}
|
|
|
|
const items = res.functions.map(convertFunctionToGraphQLReturn)
|
|
|
|
return {
|
|
cursor: undefined,
|
|
totalCount: res.functions.length,
|
|
items
|
|
}
|
|
} catch (e) {
|
|
const isNotFound =
|
|
e instanceof ExecutionEngineFailedResponseError &&
|
|
e.response.statusMessage === 'FunctionNotFound'
|
|
if (e instanceof ExecutionEngineNetworkError || isNotFound) {
|
|
return {
|
|
cursor: null,
|
|
totalCount: 0,
|
|
items: []
|
|
}
|
|
}
|
|
|
|
throw e
|
|
}
|
|
}
|
|
},
|
|
UserAutomateInfo: {
|
|
hasAutomateGithubApp: async (parent, _args, ctx) => {
|
|
const userId = parent.userId
|
|
|
|
let hasAutomateGithubApp = false
|
|
try {
|
|
const authState = await getUserGithubAuthState({ userId })
|
|
hasAutomateGithubApp = authState.userHasAuthorizedGitHubApp
|
|
} catch (e) {
|
|
ctx.log.error(e, 'Failed to resolve user automate github auth state')
|
|
}
|
|
|
|
return hasAutomateGithubApp
|
|
},
|
|
availableGithubOrgs: async (parent, _args, ctx) => {
|
|
const userId = parent.userId
|
|
const authCode = await createStoredAuthCodeFactory({
|
|
redis: getGenericRedis()
|
|
})({
|
|
userId,
|
|
action: AuthCodePayloadAction.GetAvailableGithubOrganizations
|
|
})
|
|
|
|
let orgs: string[] = []
|
|
try {
|
|
orgs = (
|
|
await getUserGithubOrganizations({
|
|
authCode
|
|
})
|
|
).availableGitHubOrganisations
|
|
} catch (e) {
|
|
let isSeriousError = true
|
|
|
|
if (e instanceof ExecutionEngineFailedResponseError) {
|
|
if (e.response.statusMessage === 'InvalidOrMissingGithubAuth') {
|
|
isSeriousError = false
|
|
}
|
|
}
|
|
|
|
if (isSeriousError) {
|
|
ctx.log.error(e, 'Failed to resolve user automate github orgs')
|
|
}
|
|
}
|
|
|
|
return orgs
|
|
}
|
|
},
|
|
ServerInfo: {
|
|
// TODO: Needs proper integration w/ Execution engine
|
|
automate: () => ({
|
|
availableFunctionTemplates: functionTemplateRepos.slice()
|
|
})
|
|
},
|
|
ProjectMutations: {
|
|
async automationMutations(_parent, { projectId }, ctx) {
|
|
await validateStreamAccess(
|
|
ctx.userId,
|
|
projectId,
|
|
Roles.Stream.Contributor,
|
|
ctx.resourceAccessRules
|
|
)
|
|
return { projectId }
|
|
}
|
|
},
|
|
Mutation: {
|
|
async automateFunctionRunStatusReport(_parent, { input }, ctx) {
|
|
const projectId = input.projectId
|
|
const functionRunId = input.functionRunId
|
|
const logger = ctx.log.child({
|
|
projectId,
|
|
streamId: projectId, //legacy
|
|
functionRunId
|
|
})
|
|
|
|
const projectDb = await getProjectDbClient({ projectId: input.projectId })
|
|
const reportFunctionRunStatus = reportFunctionRunStatusFactory({
|
|
getAutomationFunctionRunRecord: getFunctionRunFactory({
|
|
db: projectDb
|
|
}),
|
|
upsertAutomationFunctionRunRecord: upsertAutomationFunctionRunFactory({
|
|
db: projectDb
|
|
}),
|
|
automationRunUpdater: updateAutomationRunFactory({
|
|
db: projectDb
|
|
}),
|
|
emitEvent: getEventBus().emit
|
|
})
|
|
|
|
const result = await withOperationLogging(
|
|
async () =>
|
|
await reportFunctionRunStatus({
|
|
...input,
|
|
contextView: input.contextView ?? null,
|
|
results:
|
|
(input.results as Automate.AutomateTypes.ResultsSchema) ?? null,
|
|
runId: input.functionRunId,
|
|
status: mapGqlStatusToDbStatus(input.status),
|
|
statusMessage: input.statusMessage ?? null
|
|
}),
|
|
{
|
|
logger,
|
|
operationName: 'automateFunctionRunStatusReport',
|
|
operationDescription: 'Report the status of a function run'
|
|
}
|
|
)
|
|
|
|
return result
|
|
},
|
|
automateMutations: () => ({})
|
|
},
|
|
Subscription: {
|
|
projectTriggeredAutomationsStatusUpdated: {
|
|
subscribe: filteredSubscribe(
|
|
ProjectSubscriptions.ProjectTriggeredAutomationsStatusUpdated,
|
|
async (payload, args, ctx) => {
|
|
if (payload.projectId !== args.projectId) return false
|
|
|
|
await authorizeResolver(
|
|
ctx.userId,
|
|
payload.projectId,
|
|
Roles.Stream.Owner,
|
|
ctx.resourceAccessRules
|
|
)
|
|
return true
|
|
}
|
|
)
|
|
},
|
|
projectAutomationsUpdated: {
|
|
subscribe: filteredSubscribe(
|
|
ProjectSubscriptions.ProjectAutomationsUpdated,
|
|
async (payload, args, ctx) => {
|
|
if (payload.projectId !== args.projectId) return false
|
|
|
|
await authorizeResolver(
|
|
ctx.userId,
|
|
payload.projectId,
|
|
Roles.Stream.Owner,
|
|
ctx.resourceAccessRules
|
|
)
|
|
return true
|
|
}
|
|
)
|
|
}
|
|
}
|
|
}
|
|
: {
|
|
/**
|
|
* If automate module is disabled
|
|
*/
|
|
Project: {
|
|
automation: () => {
|
|
throw new AutomateApiDisabledError()
|
|
}
|
|
},
|
|
AutomateMutations: {
|
|
createFunction: () => {
|
|
throw new AutomateApiDisabledError()
|
|
},
|
|
updateFunction: () => {
|
|
throw new AutomateApiDisabledError()
|
|
}
|
|
},
|
|
ProjectAutomationMutations: {
|
|
create: () => {
|
|
throw new AutomateApiDisabledError()
|
|
},
|
|
update: () => {
|
|
throw new AutomateApiDisabledError()
|
|
},
|
|
createRevision: () => {
|
|
throw new AutomateApiDisabledError()
|
|
},
|
|
trigger: () => {
|
|
throw new AutomateApiDisabledError()
|
|
}
|
|
},
|
|
Query: {
|
|
automateValidateAuthCode: () => {
|
|
throw new AutomateApiDisabledError()
|
|
},
|
|
automateFunction: () => {
|
|
throw new AutomateApiDisabledError()
|
|
},
|
|
automateFunctions: () => {
|
|
throw new AutomateApiDisabledError()
|
|
}
|
|
},
|
|
User: {
|
|
automateInfo: () => {
|
|
throw new AutomateApiDisabledError()
|
|
}
|
|
},
|
|
ServerInfo: {
|
|
automate: () => ({
|
|
availableFunctionTemplates: []
|
|
})
|
|
},
|
|
Mutation: {
|
|
automateFunctionRunStatusReport: () => {
|
|
throw new AutomateApiDisabledError()
|
|
},
|
|
automateMutations: () => ({})
|
|
},
|
|
Subscription: {
|
|
projectTriggeredAutomationsStatusUpdated: {
|
|
subscribe: filteredSubscribe(
|
|
ProjectSubscriptions.ProjectTriggeredAutomationsStatusUpdated,
|
|
() => false
|
|
)
|
|
},
|
|
projectAutomationsUpdated: {
|
|
subscribe: filteredSubscribe(
|
|
ProjectSubscriptions.ProjectAutomationsUpdated,
|
|
() => false
|
|
)
|
|
}
|
|
}
|
|
}) as Resolvers
|