diff --git a/packages/server/modules/automate/domain/operations.ts b/packages/server/modules/automate/domain/operations.ts index 160c88d0b..1c9be6aa8 100644 --- a/packages/server/modules/automate/domain/operations.ts +++ b/packages/server/modules/automate/domain/operations.ts @@ -1,7 +1,9 @@ import { AutomationRecord, + AutomationRevisionWithTriggersFunctions, AutomationTokenRecord } from '@/modules/automate/helpers/types' +import { InsertableAutomationRevision } from '@/modules/automate/repositories/automations' import { AuthCodePayload } from '@/modules/automate/services/authCode' import { ProjectAutomationCreateInput } from '@/modules/core/graph/generated/graphql' import { ContextResourceAccessRules } from '@/modules/core/helpers/token' @@ -14,6 +16,10 @@ export type StoreAutomationToken = ( automationToken: AutomationTokenRecord ) => Promise +export type StoreAutomationRevision = ( + revision: InsertableAutomationRevision +) => Promise + export type CreateStoredAuthCode = ( params: Omit ) => Promise @@ -24,3 +30,12 @@ export type CreateAutomation = (params: { userId: string userResourceAccessRules?: ContextResourceAccessRules }) => Promise<{ automation: AutomationRecord; token: AutomationTokenRecord }> + +type KeyPair = { + publicKey: string + privateKey: string +} + +export type GetEncryptionKeyPair = () => Promise + +export type GetEncryptionKeyPairFor = (publicKey: string) => Promise diff --git a/packages/server/modules/automate/graph/resolvers/automate.ts b/packages/server/modules/automate/graph/resolvers/automate.ts index 6f6818229..885a6c566 100644 --- a/packages/server/modules/automate/graph/resolvers/automate.ts +++ b/packages/server/modules/automate/graph/resolvers/automate.ts @@ -22,7 +22,7 @@ import { getProjectAutomationsItems, getProjectAutomationsTotalCount, storeAutomationFactory, - storeAutomationRevision, + storeAutomationRevisionFactory, storeAutomationTokenFactory, updateAutomationRun, updateAutomation as updateDbAutomation, @@ -31,7 +31,7 @@ import { import { createAutomationFactory, createAutomationRevision, - createTestAutomation, + createTestAutomationFactory, getAutomationsStatus, updateAutomation } from '@/modules/automate/services/automationManagement' @@ -110,6 +110,7 @@ const { FF_AUTOMATE_MODULE_ENABLED } = getFeatureFlags() const storeAutomation = storeAutomationFactory({ db }) const storeAutomationToken = storeAutomationTokenFactory({ db }) +const storeAutomationRevision = storeAutomationRevisionFactory({ db }) export = (FF_AUTOMATE_MODULE_ENABLED ? { @@ -532,11 +533,13 @@ export = (FF_AUTOMATE_MODULE_ENABLED return automationRunId }, async createTestAutomation(parent, { input }, ctx) { - const create = createTestAutomation({ + const create = createTestAutomationFactory({ getEncryptionKeyPair, getFunction, storeAutomation, - storeAutomationRevision + storeAutomationRevision, + validateStreamAccess, + automationsEventsEmit: AutomationsEmitter.emit }) return await create({ diff --git a/packages/server/modules/automate/repositories/automations.ts b/packages/server/modules/automate/repositories/automations.ts index 2fcd044df..0dc323e16 100644 --- a/packages/server/modules/automate/repositories/automations.ts +++ b/packages/server/modules/automate/repositories/automations.ts @@ -1,5 +1,6 @@ import { StoreAutomation, + StoreAutomationRevision, StoreAutomationToken } from '@/modules/automate/domain/operations' import { @@ -57,7 +58,13 @@ import { SetOptional, SetRequired } from 'type-fest' const tables = { automations: (db: Knex) => db(Automations.name), - automationTokens: (db: Knex) => db(AutomationTokens.name) + automationTokens: (db: Knex) => db(AutomationTokens.name), + automationRevisions: (db: Knex) => + db(AutomationRevisions.name), + automationRevisionFunctions: (db: Knex) => + db(AutomationRevisionFunctions.name), + automationTriggers: (db: Knex) => + db(AutomationTriggers.name) } export const generateRevisionId = () => cryptoRandomString({ length: 10 }) @@ -337,57 +344,61 @@ export async function updateAutomationRevision( return ret } -export type StoredInsertableAutomationRevision = Awaited< - ReturnType -> +export type StoredInsertableAutomationRevision = AutomationRevisionWithTriggersFunctions -export async function storeAutomationRevision(revision: InsertableAutomationRevision) { - const id = revision.id || generateRevisionId() - const rev = _.pick(revision, AutomationRevisions.withoutTablePrefix.cols) - const [newRev] = await AutomationRevisions.knex() - .insert({ - ...rev, - id - }) - .returning('*') - const [functions, triggers] = await Promise.all([ - AutomationRevisionFunctions.knex() - .insert( - revision.functions.map( - (f): AutomateRevisionFunctionRecord => ({ - ...f, - automationRevisionId: id - }) +export const storeAutomationRevisionFactory = + (deps: { db: Knex }): StoreAutomationRevision => + async (revision: InsertableAutomationRevision) => { + const id = revision.id || generateRevisionId() + const rev = _.pick(revision, AutomationRevisions.withoutTablePrefix.cols) + const [newRev] = await tables + .automationRevisions(deps.db) + .insert({ + ...rev, + id + }) + .returning('*') + const [functions, triggers] = await Promise.all([ + tables + .automationRevisionFunctions(deps.db) + .insert( + revision.functions.map( + (f): AutomateRevisionFunctionRecord => ({ + ...f, + automationRevisionId: id + }) + ) ) - ) - .returning('*'), - AutomationTriggers.knex() - .insert( - revision.triggers.map( - (t): AutomationTriggerDefinitionRecord => ({ - ...t, - automationRevisionId: id - }) + .returning('*'), + tables + .automationTriggers(deps.db) + .insert( + revision.triggers.map( + (t): AutomationTriggerDefinitionRecord => ({ + ...t, + automationRevisionId: id + }) + ) ) - ) - .returning('*'), - // 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) - ] - : []) - ]) + .returning('*'), + // Unset 'active in revision' for all other revisions + ...(revision.active + ? [ + tables + .automationRevisions(deps.db) + .where(AutomationRevisions.col.automationId, newRev.automationId) + .andWhereNot(AutomationRevisions.col.id, newRev.id) + .update(AutomationRevisions.withoutTablePrefix.col.active, false) + ] + : []) + ]) - return { - ...newRev, - functions, - triggers + return { + ...newRev, + functions, + triggers + } } -} export async function getAutomationToken( automationId: string diff --git a/packages/server/modules/automate/services/automationManagement.ts b/packages/server/modules/automate/services/automationManagement.ts index 69b6ab62b..e68cad003 100644 --- a/packages/server/modules/automate/services/automationManagement.ts +++ b/packages/server/modules/automate/services/automationManagement.ts @@ -4,7 +4,6 @@ import { InsertableAutomationRevisionTrigger, getAutomation, getLatestVersionAutomationRuns, - storeAutomationRevision, updateAutomation as updateDbAutomation } from '@/modules/automate/repositories/automations' import { getServerOrigin } from '@/modules/shared/helpers/envHelper' @@ -41,10 +40,7 @@ import { getBranchesByIds } from '@/modules/core/repositories/branches' import { keyBy, uniq } from 'lodash' import { resolveStatusFromFunctionRunStatuses } from '@/modules/automate/services/runsManagement' import { TriggeredAutomationsStatusGraphQLReturn } from '@/modules/automate/helpers/graphTypes' -import { - getEncryptionKeyPair, - getFunctionInputDecryptor -} from '@/modules/automate/services/encryption' +import { getFunctionInputDecryptor } from '@/modules/automate/services/encryption' import { LibsodiumEncryptionError } from '@/modules/shared/errors/encryption' import { validateInputAgainstFunctionSchema } from '@/modules/automate/utils/inputSchemaValidator' import { @@ -55,7 +51,9 @@ import { validateAutomationName } from '@/modules/automate/utils/automationConfi import { CreateAutomation, CreateStoredAuthCode, + GetEncryptionKeyPair, StoreAutomation, + StoreAutomationRevision, StoreAutomationToken } from '@/modules/automate/domain/operations' @@ -139,17 +137,19 @@ export const createAutomationFactory = } export type CreateTestAutomationDeps = { - getEncryptionKeyPair: typeof getEncryptionKeyPair + getEncryptionKeyPair: GetEncryptionKeyPair getFunction: typeof getFunction storeAutomation: StoreAutomation - storeAutomationRevision: typeof storeAutomationRevision + storeAutomationRevision: StoreAutomationRevision + validateStreamAccess: typeof validateStreamAccess + automationsEventsEmit: AutomationsEventsEmit } /** * Create a test automation and its first revision in one request. * TODO: Reduce code duplication w/ createAutomation */ -export const createTestAutomation = +export const createTestAutomationFactory = (deps: CreateTestAutomationDeps) => async (params: { input: ProjectTestAutomationCreateInput @@ -167,7 +167,9 @@ export const createTestAutomation = getEncryptionKeyPair, getFunction, storeAutomation, - storeAutomationRevision + storeAutomationRevision, + validateStreamAccess, + automationsEventsEmit } = deps validateAutomationName(name) @@ -233,7 +235,7 @@ export const createTestAutomation = publicKey: encryptionKeyPair.publicKey }) - await AutomationsEmitter.emit(AutomationsEmitter.events.CreatedRevision, { + await automationsEventsEmit(AutomationsEmitter.events.CreatedRevision, { automation: automationRecord, revision: automationRevisionRecord }) @@ -382,8 +384,8 @@ const validateNewRevisionFunctions = export type CreateAutomationRevisionDeps = { getAutomation: typeof getAutomation - storeAutomationRevision: typeof storeAutomationRevision - getEncryptionKeyPair: typeof getEncryptionKeyPair + storeAutomationRevision: StoreAutomationRevision + getEncryptionKeyPair: GetEncryptionKeyPair getFunctionInputDecryptor: ReturnType getFunctionReleases: typeof getFunctionReleases } & ValidateNewTriggerDefinitionsDeps & diff --git a/packages/server/modules/automate/services/encryption.ts b/packages/server/modules/automate/services/encryption.ts index bc7e5927d..a271fe8e0 100644 --- a/packages/server/modules/automate/services/encryption.ts +++ b/packages/server/modules/automate/services/encryption.ts @@ -18,6 +18,10 @@ import { LibsodiumEncryptionError } from '@/modules/shared/errors/encryption' import { Merge } from 'type-fest' import { convertFunctionReleaseToGraphQLReturn } from '@/modules/automate/services/functionManagement' import { redactWriteOnlyInputData } from '@/modules/automate/utils/jsonSchemaRedactor' +import { + GetEncryptionKeyPair, + GetEncryptionKeyPairFor +} from '@/modules/automate/domain/operations' type KeysFileContents = Array @@ -49,7 +53,7 @@ const getEncryptionKeys = async () => { return keys } -export const getEncryptionKeyPair = async () => { +export const getEncryptionKeyPair: GetEncryptionKeyPair = async () => { return (await getEncryptionKeys())[0] } @@ -57,7 +61,9 @@ export const getEncryptionPublicKey = async () => { return (await getEncryptionKeyPair()).publicKey } -export const getEncryptionKeyPairFor = async (publicKey: string) => { +export const getEncryptionKeyPairFor: GetEncryptionKeyPairFor = async ( + publicKey: string +) => { const keyPairs = await getEncryptionKeys() const keyPair = keyPairs.find((keyPair) => keyPair.publicKey === publicKey) if (!keyPair) { @@ -139,7 +145,7 @@ export const getFunctionInputDecryptor = } export type GetFunctionInputsForFrontendDeps = { - getEncryptionKeyPairFor: typeof getEncryptionKeyPairFor + getEncryptionKeyPairFor: GetEncryptionKeyPairFor redactWriteOnlyInputData: typeof redactWriteOnlyInputData } & GetFunctionInputDecryptorDeps diff --git a/packages/server/modules/automate/services/trigger.ts b/packages/server/modules/automate/services/trigger.ts index 8154f4c9f..471b20768 100644 --- a/packages/server/modules/automate/services/trigger.ts +++ b/packages/server/modules/automate/services/trigger.ts @@ -40,12 +40,10 @@ import { validateStreamAccess } from '@/modules/core/services/streams/streamAcce import { ContextResourceAccessRules } from '@/modules/core/helpers/token' import { TokenResourceIdentifierType } from '@/modules/core/graph/generated/graphql' import { automateLogger } from '@/logging/logging' -import { - getEncryptionKeyPairFor, - getFunctionInputDecryptor -} from '@/modules/automate/services/encryption' +import { getFunctionInputDecryptor } from '@/modules/automate/services/encryption' import { LibsodiumEncryptionError } from '@/modules/shared/errors/encryption' import { AutomateRunsEmitter } from '@/modules/automate/events/runs' +import { GetEncryptionKeyPairFor } from '@/modules/automate/domain/operations' export type OnModelVersionCreateDeps = { getAutomation: typeof getAutomation @@ -126,7 +124,7 @@ type InsertableAutomationRunWithExtendedFunctionRuns = Merge< > type CreateAutomationRunDataDeps = { - getEncryptionKeyPairFor: typeof getEncryptionKeyPairFor + getEncryptionKeyPairFor: GetEncryptionKeyPairFor getFunctionInputDecryptor: ReturnType } diff --git a/packages/server/modules/automate/tests/trigger.spec.ts b/packages/server/modules/automate/tests/trigger.spec.ts index 158917f1f..80dc2ecb1 100644 --- a/packages/server/modules/automate/tests/trigger.spec.ts +++ b/packages/server/modules/automate/tests/trigger.spec.ts @@ -37,14 +37,14 @@ import { getFullAutomationRunById, getAutomationTriggerDefinitions, getFunctionRun, - storeAutomationRevision, updateAutomation, updateAutomationRevision, updateAutomationRun, upsertAutomationRun, upsertAutomationFunctionRun, storeAutomationFactory, - storeAutomationTokenFactory + storeAutomationTokenFactory, + storeAutomationRevisionFactory } from '@/modules/automate/repositories/automations' import { beforeEachContext, truncateTables } from '@/test/hooks' import { Automate } from '@speckle/shared' @@ -77,6 +77,7 @@ const { FF_AUTOMATE_MODULE_ENABLED } = getFeatureFlags() const storeAutomation = storeAutomationFactory({ db }) const storeAutomationToken = storeAutomationTokenFactory({ db }) +const storeAutomationRevision = storeAutomationRevisionFactory({ db }) ;(FF_AUTOMATE_MODULE_ENABLED ? describe : describe.skip)( 'Automate triggers @automate', diff --git a/packages/server/test/speckle-helpers/automationHelper.ts b/packages/server/test/speckle-helpers/automationHelper.ts index 6dbc4a9fc..ba9f13c65 100644 --- a/packages/server/test/speckle-helpers/automationHelper.ts +++ b/packages/server/test/speckle-helpers/automationHelper.ts @@ -1,7 +1,7 @@ import { getAutomation, storeAutomationFactory, - storeAutomationRevision, + storeAutomationRevisionFactory, storeAutomationTokenFactory } from '@/modules/automate/repositories/automations' import { @@ -44,6 +44,7 @@ import { validateStreamAccess } from '@/modules/core/services/streams/streamAcce const storeAutomation = storeAutomationFactory({ db }) const storeAutomationToken = storeAutomationTokenFactory({ db }) +const storeAutomationRevision = storeAutomationRevisionFactory({ db }) export const generateFunctionId = () => cryptoRandomString({ length: 10 }) export const generateFunctionReleaseId = () => cryptoRandomString({ length: 10 })