diff --git a/packages/frontend-2/lib/automations/composables/automationsStatus.ts b/packages/frontend-2/lib/automations/composables/automationsStatus.ts new file mode 100644 index 000000000..602caeee6 --- /dev/null +++ b/packages/frontend-2/lib/automations/composables/automationsStatus.ts @@ -0,0 +1,51 @@ +import { ApolloCache } from '@apollo/client/core' +import { OnModelVersionCardAutomationsStatusUpdatedSubscription } from '~~/lib/common/generated/gql/graphql' +import { Get } from 'type-fest' +import { onModelVersionCardAutomationsStatusUpdated } from '~~/lib/automations/graphql/subscriptions' +import { useApolloClient, useSubscription } from '@vue/apollo-composable' +import { useLock } from '~~/lib/common/composables/singleton' + +/** + * Track project model/version automations status updates and makes cache updates accordingly. + * Optionally you can provide an extra handler to be called when an event is received. + */ +export const useModelVersionCardAutomationsStatusUpdateTracking = ( + projectId: MaybeRef, + handler?: ( + data: NonNullable< + Get< + OnModelVersionCardAutomationsStatusUpdatedSubscription, + 'projectAutomationsStatusUpdated' + > + >, + cache: ApolloCache + ) => void +) => { + const { onResult } = useSubscription( + onModelVersionCardAutomationsStatusUpdated, + () => ({ + projectId: unref(projectId) + }) + ) + + const { hasLock } = useLock( + computed( + () => `useModelVersionCardAutomationsStatusUpdateTracking-${unref(projectId)}` + ) + ) + + const apollo = useApolloClient().client + + onResult((result) => { + if (!result.data?.projectAutomationsStatusUpdated || !hasLock.value) return + + // Just by virtue of receiving this event the cache should be updated + // In case we need to do any global stuff, feel free to do it below: + }) + + onResult((result) => { + if (!result.data?.projectAutomationsStatusUpdated) return + const event = result.data.projectAutomationsStatusUpdated + handler?.(event, apollo.cache) + }) +} diff --git a/packages/frontend-2/lib/automations/graphql/subscriptions.ts b/packages/frontend-2/lib/automations/graphql/subscriptions.ts new file mode 100644 index 000000000..08ec55d05 --- /dev/null +++ b/packages/frontend-2/lib/automations/graphql/subscriptions.ts @@ -0,0 +1,11 @@ +import { graphql } from '~~/lib/common/generated/gql' + +export const onModelVersionCardAutomationsStatusUpdated = graphql(` + subscription OnModelVersionCardAutomationsStatusUpdated($projectId: String!) { + projectAutomationsStatusUpdated(projectId: $projectId) { + status { + ...ModelCardAutomationStatus_AutomationsStatus + } + } + } +`) diff --git a/packages/frontend-2/lib/common/generated/gql/gql.ts b/packages/frontend-2/lib/common/generated/gql/gql.ts index db90854cb..cf54a07de 100644 --- a/packages/frontend-2/lib/common/generated/gql/gql.ts +++ b/packages/frontend-2/lib/common/generated/gql/gql.ts @@ -70,6 +70,7 @@ const documents = { "\n mutation FinishOnboarding {\n activeUserMutations {\n finishOnboarding\n }\n }\n": types.FinishOnboardingDocument, "\n query AuthServerInfo {\n serverInfo {\n ...AuthStategiesServerInfoFragment\n ...ServerTermsOfServicePrivacyPolicyFragment\n ...AuthRegisterPanelServerInfo\n }\n }\n": types.AuthServerInfoDocument, "\n query AuthorizableAppMetadata($id: String!) {\n app(id: $id) {\n id\n name\n description\n trustByDefault\n redirectUrl\n scopes {\n name\n description\n }\n author {\n name\n id\n avatar\n }\n }\n }\n": types.AuthorizableAppMetadataDocument, + "\n subscription OnModelVersionCardAutomationsStatusUpdated($projectId: String!) {\n projectAutomationsStatusUpdated(projectId: $projectId) {\n status {\n ...ModelCardAutomationStatus_AutomationsStatus\n }\n }\n }\n": types.OnModelVersionCardAutomationsStatusUpdatedDocument, "\n query MentionsUserSearch($query: String!, $emailOnly: Boolean = false) {\n userSearch(\n query: $query\n limit: 5\n cursor: null\n archived: false\n emailOnly: $emailOnly\n ) {\n items {\n id\n name\n company\n }\n }\n }\n": types.MentionsUserSearchDocument, "\n query UserSearch($query: String!, $limit: Int, $cursor: String, $archived: Boolean) {\n userSearch(query: $query, limit: $limit, cursor: $cursor, archived: $archived) {\n cursor\n items {\n id\n name\n bio\n company\n avatar\n verified\n role\n }\n }\n }\n": types.UserSearchDocument, "\n query ServerInfoBlobSizeLimit {\n serverInfo {\n blobSizeLimitBytes\n }\n }\n": types.ServerInfoBlobSizeLimitDocument, @@ -396,6 +397,10 @@ export function graphql(source: "\n query AuthServerInfo {\n serverInfo {\n * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ export function graphql(source: "\n query AuthorizableAppMetadata($id: String!) {\n app(id: $id) {\n id\n name\n description\n trustByDefault\n redirectUrl\n scopes {\n name\n description\n }\n author {\n name\n id\n avatar\n }\n }\n }\n"): (typeof documents)["\n query AuthorizableAppMetadata($id: String!) {\n app(id: $id) {\n id\n name\n description\n trustByDefault\n redirectUrl\n scopes {\n name\n description\n }\n author {\n name\n id\n avatar\n }\n }\n }\n"]; +/** + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function graphql(source: "\n subscription OnModelVersionCardAutomationsStatusUpdated($projectId: String!) {\n projectAutomationsStatusUpdated(projectId: $projectId) {\n status {\n ...ModelCardAutomationStatus_AutomationsStatus\n }\n }\n }\n"): (typeof documents)["\n subscription OnModelVersionCardAutomationsStatusUpdated($projectId: String!) {\n projectAutomationsStatusUpdated(projectId: $projectId) {\n status {\n ...ModelCardAutomationStatus_AutomationsStatus\n }\n }\n }\n"]; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ diff --git a/packages/frontend-2/lib/common/generated/gql/graphql.ts b/packages/frontend-2/lib/common/generated/gql/graphql.ts index 6f13dbf5b..f69995166 100644 --- a/packages/frontend-2/lib/common/generated/gql/graphql.ts +++ b/packages/frontend-2/lib/common/generated/gql/graphql.ts @@ -1463,6 +1463,14 @@ export type ProjectWebhooksArgs = { id?: InputMaybe; }; +export type ProjectAutomationsStatusUpdatedMessage = { + __typename?: 'ProjectAutomationsStatusUpdatedMessage'; + model: Model; + project: Project; + status: AutomationsStatus; + version: Version; +}; + export type ProjectCollaborator = { __typename?: 'ProjectCollaborator'; role: Scalars['String']; @@ -2329,6 +2337,7 @@ export type Subscription = { commitDeleted?: Maybe; /** Subscribe to commit updated event. */ commitUpdated?: Maybe; + projectAutomationsStatusUpdated: ProjectAutomationsStatusUpdatedMessage; /** * Subscribe to updates to resource comments/threads. Optionally specify resource ID string to only receive * updates regarding comments for those resources. @@ -2416,6 +2425,11 @@ export type SubscriptionCommitUpdatedArgs = { }; +export type SubscriptionProjectAutomationsStatusUpdatedArgs = { + projectId: Scalars['String']; +}; + + export type SubscriptionProjectCommentsUpdatedArgs = { target: ViewerUpdateTrackingTarget; }; @@ -2965,6 +2979,13 @@ export type AuthorizableAppMetadataQueryVariables = Exact<{ export type AuthorizableAppMetadataQuery = { __typename?: 'Query', app?: { __typename?: 'ServerApp', id: string, name: string, description?: string | null, trustByDefault?: boolean | null, redirectUrl: string, scopes: Array<{ __typename?: 'Scope', name: string, description: string }>, author?: { __typename?: 'AppAuthor', name?: string | null, id?: string | null, avatar?: string | null } | null } | null }; +export type OnModelVersionCardAutomationsStatusUpdatedSubscriptionVariables = Exact<{ + projectId: Scalars['String']; +}>; + + +export type OnModelVersionCardAutomationsStatusUpdatedSubscription = { __typename?: 'Subscription', projectAutomationsStatusUpdated: { __typename?: 'ProjectAutomationsStatusUpdatedMessage', status: { __typename?: 'AutomationsStatus', id: string, status: AutomationRunStatus, statusMessage?: string | null, automationRuns: Array<{ __typename?: 'AutomationRun', id: string, automationId: string, createdAt: string, status: AutomationRunStatus, functionRuns: Array<{ __typename?: 'AutomationFunctionRun', id: string, functionId: string, elapsed: number, status: AutomationRunStatus, statusMessage?: string | null, contextView?: string | null, resultVersions: Array<{ __typename?: 'Version', id: string }> }> }> } } }; + export type MentionsUserSearchQueryVariables = Exact<{ query: Scalars['String']; emailOnly?: InputMaybe; @@ -3586,6 +3607,7 @@ export const CreateOnboardingProjectDocument = {"kind":"Document","definitions": export const FinishOnboardingDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"FinishOnboarding"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"activeUserMutations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"finishOnboarding"}}]}}]}}]} as unknown as DocumentNode; export const AuthServerInfoDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"AuthServerInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"serverInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"AuthStategiesServerInfoFragment"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"ServerTermsOfServicePrivacyPolicyFragment"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"AuthRegisterPanelServerInfo"}}]}}]}},...AuthStategiesServerInfoFragmentFragmentDoc.definitions,...ServerTermsOfServicePrivacyPolicyFragmentFragmentDoc.definitions,...AuthRegisterPanelServerInfoFragmentDoc.definitions]} as unknown as DocumentNode; export const AuthorizableAppMetadataDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"AuthorizableAppMetadata"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"app"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"trustByDefault"}},{"kind":"Field","name":{"kind":"Name","value":"redirectUrl"}},{"kind":"Field","name":{"kind":"Name","value":"scopes"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}}]}},{"kind":"Field","name":{"kind":"Name","value":"author"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"avatar"}}]}}]}}]}}]} as unknown as DocumentNode; +export const OnModelVersionCardAutomationsStatusUpdatedDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"subscription","name":{"kind":"Name","value":"OnModelVersionCardAutomationsStatusUpdated"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"projectId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"projectAutomationsStatusUpdated"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"projectId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"projectId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"status"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ModelCardAutomationStatus_AutomationsStatus"}}]}}]}}]}},...ModelCardAutomationStatus_AutomationsStatusFragmentDoc.definitions]} as unknown as DocumentNode; export const MentionsUserSearchDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"MentionsUserSearch"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"query"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"emailOnly"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Boolean"}},"defaultValue":{"kind":"BooleanValue","value":false}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"userSearch"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"query"},"value":{"kind":"Variable","name":{"kind":"Name","value":"query"}}},{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"IntValue","value":"5"}},{"kind":"Argument","name":{"kind":"Name","value":"cursor"},"value":{"kind":"NullValue"}},{"kind":"Argument","name":{"kind":"Name","value":"archived"},"value":{"kind":"BooleanValue","value":false}},{"kind":"Argument","name":{"kind":"Name","value":"emailOnly"},"value":{"kind":"Variable","name":{"kind":"Name","value":"emailOnly"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"company"}}]}}]}}]}}]} as unknown as DocumentNode; export const UserSearchDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"UserSearch"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"query"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"limit"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"cursor"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"archived"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Boolean"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"userSearch"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"query"},"value":{"kind":"Variable","name":{"kind":"Name","value":"query"}}},{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"Variable","name":{"kind":"Name","value":"limit"}}},{"kind":"Argument","name":{"kind":"Name","value":"cursor"},"value":{"kind":"Variable","name":{"kind":"Name","value":"cursor"}}},{"kind":"Argument","name":{"kind":"Name","value":"archived"},"value":{"kind":"Variable","name":{"kind":"Name","value":"archived"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"cursor"}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"bio"}},{"kind":"Field","name":{"kind":"Name","value":"company"}},{"kind":"Field","name":{"kind":"Name","value":"avatar"}},{"kind":"Field","name":{"kind":"Name","value":"verified"}},{"kind":"Field","name":{"kind":"Name","value":"role"}}]}}]}}]}}]} as unknown as DocumentNode; export const ServerInfoBlobSizeLimitDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ServerInfoBlobSizeLimit"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"serverInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"blobSizeLimitBytes"}}]}}]}}]} as unknown as DocumentNode; diff --git a/packages/frontend-2/lib/projects/composables/projectPages.ts b/packages/frontend-2/lib/projects/composables/projectPages.ts index e340f0fa7..dc6c492d4 100644 --- a/packages/frontend-2/lib/projects/composables/projectPages.ts +++ b/packages/frontend-2/lib/projects/composables/projectPages.ts @@ -1,4 +1,5 @@ import { MaybeRef } from '@vueuse/core' +import { useModelVersionCardAutomationsStatusUpdateTracking } from '~~/lib/automations/composables/automationsStatus' import { useSynchronizedCookie } from '~~/lib/common/composables/reactiveCookie' import { GridListToggleValue } from '~~/lib/layout/helpers/components' import { @@ -60,4 +61,8 @@ export function useGeneralProjectPageUpdateTracking( // Pending model & version update tracking useProjectPendingVersionUpdateTracking(projectId) useProjectPendingModelUpdateTracking(projectId) + + // AUTOMATIONS: + // AutomationsStatus update + useModelVersionCardAutomationsStatusUpdateTracking(projectId) } diff --git a/packages/server/assets/automations/typedefs/automation.graphql b/packages/server/assets/automations/typedefs/automation.graphql index ed1fa1a6a..e2d5bdbd7 100644 --- a/packages/server/assets/automations/typedefs/automation.graphql +++ b/packages/server/assets/automations/typedefs/automation.graphql @@ -117,3 +117,16 @@ type AutomationMutations { extend type Mutation { automationMutations: AutomationMutations! } + +type ProjectAutomationsStatusUpdatedMessage { + status: AutomationsStatus! + version: Version! + model: Model! + project: Project! +} + +extend type Subscription { + projectAutomationsStatusUpdated( + projectId: String! + ): ProjectAutomationsStatusUpdatedMessage! +} diff --git a/packages/server/modules/automations/graph/resolvers/automations.ts b/packages/server/modules/automations/graph/resolvers/automations.ts index 7f8b664a4..e79cd992d 100644 --- a/packages/server/modules/automations/graph/resolvers/automations.ts +++ b/packages/server/modules/automations/graph/resolvers/automations.ts @@ -5,6 +5,12 @@ import { } from '@/modules/automations/services/management' import { formatResults } from '@/modules/automations/services/results' import { Resolvers } from '@/modules/core/graph/generated/graphql' +import { getStream } from '@/modules/core/repositories/streams' +import { + ProjectSubscriptions, + filteredSubscribe +} from '@/modules/shared/utils/subscriptions' +import { ForbiddenError } from 'apollo-server-express' export = { Model: { @@ -67,5 +73,28 @@ export = { await upsertModelAutomationRunResult({ userId, input: args.input }) return true } + }, + Subscription: { + projectAutomationsStatusUpdated: { + subscribe: filteredSubscribe( + ProjectSubscriptions.ProjectAutomationStatusUpdated, + async (payload, variables, context) => { + if (payload.projectId !== variables.projectId) return false + + const stream = await getStream({ + streamId: variables.projectId, + userId: context.userId + }) + if ( + !stream || + (!(stream.isDiscoverable || stream.isPublic) && !stream.role) + ) { + throw new ForbiddenError('You are not authorized.') + } + + return true + } + ) + } } } as Resolvers diff --git a/packages/server/modules/automations/services/management.ts b/packages/server/modules/automations/services/management.ts index 52df5e171..f15456bb8 100644 --- a/packages/server/modules/automations/services/management.ts +++ b/packages/server/modules/automations/services/management.ts @@ -30,10 +30,14 @@ import { AutomationFunctionRunGraphQLReturn } from '@/modules/automations/helper import { AutomationRunSchema } from '@/modules/automations/helpers/inputTypes' import { StreamNotFoundError } from '@/modules/core/errors/stream' import { BranchNotFoundError } from '@/modules/core/errors/branch' -import { getCommits } from '@/modules/core/repositories/commits' +import { + getCommits, + getCommit, + getCommitBranch +} from '@/modules/core/repositories/commits' import { AutomationNotFoundError } from '@/modules/automations/errors/automations' -import { getCommitById } from '@/modules/core/services/commits' import { CommitNotFoundError } from '@/modules/core/errors/commit' +import { ProjectSubscriptions, publish } from '@/modules/shared/utils/subscriptions' type AutomationRunWithFunctionRunsRecord = AutomationRunRecord & { functionRuns: AutomationFunctionRunRecord[] @@ -76,21 +80,23 @@ export async function upsertModelAutomationRunResult({ const automation = await getAutomation(input.automationId) if (!automation) throw new AutomationNotFoundError() - // authz the current user on the automation - const stream = await getStream({ - userId: userId || undefined, - streamId: automation.projectId - }) + const [stream, version, model] = await Promise.all([ + getStream({ + userId: userId || undefined, + streamId: automation.projectId + }), + getCommit(validatedInput.versionId, { + streamId: automation.projectId + }), + getCommitBranch(validatedInput.versionId) + ]) + // this is never going to happen, cause the automation has an FK to the streamId if (!stream) throw new StreamNotFoundError('Project not found') if (stream.role !== Roles.Stream.Owner) throw new ForbiddenError('Only project owners are allowed') - - const version = await getCommitById({ - streamId: automation.projectId, - id: validatedInput.versionId - }) if (!version) throw new CommitNotFoundError() + if (!model) throw new BranchNotFoundError() // store the result of the run, if it already exists, patch it const maybeAutomationRun = await getAutomationRun(input.automationRunId) @@ -153,16 +159,23 @@ export async function upsertModelAutomationRunResult({ ) await insertAutomationFunctionRunResultVersion(validVersionsRecords) - // 4. publish an event for new automation run creation - // 5. publish an event for new run result update - // the last two events should be separated. - // automate should publish the new run linked to the model / version, with function run results as pending - // the running function should only update the function run result - // this is, so that FE subscriptions can be added properly. - // when a new run is triggered, the frontend should react to that - // also for the result of independent function run results. - // we're now shortcutting this until the one automation one function barrier is there. - // publish(automationRunStatusUpdate, { foo: 'bar' }) + // Emit subscription + const newStatus = await getAutomationsStatus({ + modelId: version.branchId, + projectId: stream.id, + versionId: version.id + }) + if (newStatus) { + await publish(ProjectSubscriptions.ProjectAutomationStatusUpdated, { + projectId: stream.id, + projectAutomationsStatusUpdated: { + status: newStatus, + version, + project: stream, + model + } + }) + } } const anyFunctionRunsHaveStatus = ( diff --git a/packages/server/modules/core/graph/generated/graphql.ts b/packages/server/modules/core/graph/generated/graphql.ts index 303e235a4..58c0253c8 100644 --- a/packages/server/modules/core/graph/generated/graphql.ts +++ b/packages/server/modules/core/graph/generated/graphql.ts @@ -1476,6 +1476,14 @@ export type ProjectWebhooksArgs = { id?: InputMaybe; }; +export type ProjectAutomationsStatusUpdatedMessage = { + __typename?: 'ProjectAutomationsStatusUpdatedMessage'; + model: Model; + project: Project; + status: AutomationsStatus; + version: Version; +}; + export type ProjectCollaborator = { __typename?: 'ProjectCollaborator'; role: Scalars['String']; @@ -2342,6 +2350,7 @@ export type Subscription = { commitDeleted?: Maybe; /** Subscribe to commit updated event. */ commitUpdated?: Maybe; + projectAutomationsStatusUpdated: ProjectAutomationsStatusUpdatedMessage; /** * Subscribe to updates to resource comments/threads. Optionally specify resource ID string to only receive * updates regarding comments for those resources. @@ -2429,6 +2438,11 @@ export type SubscriptionCommitUpdatedArgs = { }; +export type SubscriptionProjectAutomationsStatusUpdatedArgs = { + projectId: Scalars['String']; +}; + + export type SubscriptionProjectCommentsUpdatedArgs = { target: ViewerUpdateTrackingTarget; }; @@ -2990,6 +3004,7 @@ export type ResolversTypes = { PasswordStrengthCheckResults: ResolverTypeWrapper; PendingStreamCollaborator: ResolverTypeWrapper; Project: ResolverTypeWrapper; + ProjectAutomationsStatusUpdatedMessage: ResolverTypeWrapper & { model: ResolversTypes['Model'], project: ResolversTypes['Project'], status: ResolversTypes['AutomationsStatus'], version: ResolversTypes['Version'] }>; ProjectCollaborator: ResolverTypeWrapper & { user: ResolversTypes['LimitedUser'] }>; ProjectCollection: ResolverTypeWrapper & { items: Array }>; ProjectCommentCollection: ResolverTypeWrapper & { items: Array }>; @@ -3160,6 +3175,7 @@ export type ResolversParentTypes = { PasswordStrengthCheckResults: PasswordStrengthCheckResults; PendingStreamCollaborator: PendingStreamCollaboratorGraphQLReturn; Project: ProjectGraphQLReturn; + ProjectAutomationsStatusUpdatedMessage: Omit & { model: ResolversParentTypes['Model'], project: ResolversParentTypes['Project'], status: ResolversParentTypes['AutomationsStatus'], version: ResolversParentTypes['Version'] }; ProjectCollaborator: Omit & { user: ResolversParentTypes['LimitedUser'] }; ProjectCollection: Omit & { items: Array }; ProjectCommentCollection: Omit & { items: Array }; @@ -3778,6 +3794,14 @@ export type ProjectResolvers; }; +export type ProjectAutomationsStatusUpdatedMessageResolvers = { + model?: Resolver; + project?: Resolver; + status?: Resolver; + version?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + export type ProjectCollaboratorResolvers = { role?: Resolver; user?: Resolver; @@ -4070,6 +4094,7 @@ export type SubscriptionResolvers, "commitCreated", ParentType, ContextType, RequireFields>; commitDeleted?: SubscriptionResolver, "commitDeleted", ParentType, ContextType, RequireFields>; commitUpdated?: SubscriptionResolver, "commitUpdated", ParentType, ContextType, RequireFields>; + projectAutomationsStatusUpdated?: SubscriptionResolver>; projectCommentsUpdated?: SubscriptionResolver>; projectModelsUpdated?: SubscriptionResolver>; projectPendingModelsUpdated?: SubscriptionResolver>; @@ -4267,6 +4292,7 @@ export type Resolvers = { PasswordStrengthCheckResults?: PasswordStrengthCheckResultsResolvers; PendingStreamCollaborator?: PendingStreamCollaboratorResolvers; Project?: ProjectResolvers; + ProjectAutomationsStatusUpdatedMessage?: ProjectAutomationsStatusUpdatedMessageResolvers; ProjectCollaborator?: ProjectCollaboratorResolvers; ProjectCollection?: ProjectCollectionResolvers; ProjectCommentCollection?: ProjectCommentCollectionResolvers; diff --git a/packages/server/modules/core/repositories/commits.ts b/packages/server/modules/core/repositories/commits.ts index 187cd798c..f578e15f8 100644 --- a/packages/server/modules/core/repositories/commits.ts +++ b/packages/server/modules/core/repositories/commits.ts @@ -38,7 +38,12 @@ export type CommitWithStreamBranchMetadata = CommitRecord & { /** * Get commits with their stream and branch IDs */ -export async function getCommits(commitIds: string[]) { +export async function getCommits( + commitIds: string[], + options?: Partial<{ streamId: string }> +) { + const { streamId } = options || {} + const q = Commits.knex() .select(CommitWithStreamBranchMetadataFields) .whereIn(Commits.col.id, commitIds) @@ -46,6 +51,10 @@ export async function getCommits(commitIds: string[]) { .leftJoin(BranchCommits.name, BranchCommits.col.commitId, Commits.col.id) .innerJoin(Branches.name, Branches.col.id, BranchCommits.col.branchId) + if (streamId) { + q.andWhere(StreamCommits.col.streamId, streamId) + } + const rows = await q // in case the join tables have multiple values for each commit @@ -55,8 +64,11 @@ export async function getCommits(commitIds: string[]) { return uniqueRows } -export async function getCommit(commitId: string) { - const [commit] = await getCommits([commitId]) +export async function getCommit( + commitId: string, + options?: Partial<{ streamId: string }> +) { + const [commit] = await getCommits([commitId], options) return commit as Optional } 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 5944f0d6c..e80e42fb6 100644 --- a/packages/server/modules/cross-server-sync/graph/generated/graphql.ts +++ b/packages/server/modules/cross-server-sync/graph/generated/graphql.ts @@ -1466,6 +1466,14 @@ export type ProjectWebhooksArgs = { id?: InputMaybe; }; +export type ProjectAutomationsStatusUpdatedMessage = { + __typename?: 'ProjectAutomationsStatusUpdatedMessage'; + model: Model; + project: Project; + status: AutomationsStatus; + version: Version; +}; + export type ProjectCollaborator = { __typename?: 'ProjectCollaborator'; role: Scalars['String']; @@ -2332,6 +2340,7 @@ export type Subscription = { commitDeleted?: Maybe; /** Subscribe to commit updated event. */ commitUpdated?: Maybe; + projectAutomationsStatusUpdated: ProjectAutomationsStatusUpdatedMessage; /** * Subscribe to updates to resource comments/threads. Optionally specify resource ID string to only receive * updates regarding comments for those resources. @@ -2419,6 +2428,11 @@ export type SubscriptionCommitUpdatedArgs = { }; +export type SubscriptionProjectAutomationsStatusUpdatedArgs = { + projectId: Scalars['String']; +}; + + export type SubscriptionProjectCommentsUpdatedArgs = { target: ViewerUpdateTrackingTarget; }; diff --git a/packages/server/modules/shared/utils/subscriptions.ts b/packages/server/modules/shared/utils/subscriptions.ts index 500551aa2..4d0169b9d 100644 --- a/packages/server/modules/shared/utils/subscriptions.ts +++ b/packages/server/modules/shared/utils/subscriptions.ts @@ -5,6 +5,9 @@ import Redis from 'ioredis' import { withFilter } from 'graphql-subscriptions' import { GraphQLContext } from '@/modules/shared/helpers/typeHelper' import { + AutomationRun, + AutomationsStatus, + ProjectAutomationsStatusUpdatedMessage, ProjectCommentsUpdatedMessage, ProjectModelsUpdatedMessage, ProjectPendingModelsUpdatedMessage, @@ -12,6 +15,7 @@ import { ProjectUpdatedMessage, ProjectVersionsPreviewGeneratedMessage, ProjectVersionsUpdatedMessage, + SubscriptionProjectAutomationsStatusUpdatedArgs, SubscriptionProjectCommentsUpdatedArgs, SubscriptionProjectModelsUpdatedArgs, SubscriptionProjectPendingModelsUpdatedArgs, @@ -33,6 +37,7 @@ import { } from '@/modules/core/helpers/graphTypes' import { CommentGraphQLReturn } from '@/modules/comments/helpers/graphTypes' import { FileUploadGraphQLReturn } from '@/modules/fileuploads/helpers/types' +import { AutomationFunctionRunGraphQLReturn } from '@/modules/automations/helpers/graphTypes' /** * GraphQL Subscription PubSub instance @@ -82,7 +87,8 @@ export enum ProjectSubscriptions { ProjectModelsUpdated = 'PROJECT_MODELS_UPDATED', ProjectVersionsUpdated = 'PROJECT_VERSIONS_UPDATED', ProjectVersionsPreviewGenerated = 'PROJECT_VERSIONS_PREVIEW_GENERATED', - ProjectCommentsUpdated = 'PROJECT_COMMENTS_UPDATED' + ProjectCommentsUpdated = 'PROJECT_COMMENTS_UPDATED', + ProjectAutomationStatusUpdated = 'PROJECT_AUTOMATION_STATUS_UPDATED' } export enum ViewerSubscriptions { @@ -184,6 +190,31 @@ type SubscriptionTypeMap = { } variables: SubscriptionProjectPendingVersionsUpdatedArgs } + [ProjectSubscriptions.ProjectAutomationStatusUpdated]: { + payload: { + projectAutomationsStatusUpdated: Merge< + ProjectAutomationsStatusUpdatedMessage, + { + version: VersionGraphQLReturn + model: ModelGraphQLReturn + project: ProjectGraphQLReturn + status: Merge< + AutomationsStatus, + { + automationRuns: Array< + Merge< + AutomationRun, + { functionRuns: AutomationFunctionRunGraphQLReturn[] } + > + > + } + > + } + > + projectId: string + } + variables: SubscriptionProjectAutomationsStatusUpdatedArgs + } } & { [k in SubscriptionEvent]: { payload: unknown; variables: unknown } } type SubscriptionEvent = diff --git a/packages/server/test/graphql/generated/graphql.ts b/packages/server/test/graphql/generated/graphql.ts index b0fa7bc97..d1c420006 100644 --- a/packages/server/test/graphql/generated/graphql.ts +++ b/packages/server/test/graphql/generated/graphql.ts @@ -1466,6 +1466,14 @@ export type ProjectWebhooksArgs = { id?: InputMaybe; }; +export type ProjectAutomationsStatusUpdatedMessage = { + __typename?: 'ProjectAutomationsStatusUpdatedMessage'; + model: Model; + project: Project; + status: AutomationsStatus; + version: Version; +}; + export type ProjectCollaborator = { __typename?: 'ProjectCollaborator'; role: Scalars['String']; @@ -2332,6 +2340,7 @@ export type Subscription = { commitDeleted?: Maybe; /** Subscribe to commit updated event. */ commitUpdated?: Maybe; + projectAutomationsStatusUpdated: ProjectAutomationsStatusUpdatedMessage; /** * Subscribe to updates to resource comments/threads. Optionally specify resource ID string to only receive * updates regarding comments for those resources. @@ -2419,6 +2428,11 @@ export type SubscriptionCommitUpdatedArgs = { }; +export type SubscriptionProjectAutomationsStatusUpdatedArgs = { + projectId: Scalars['String']; +}; + + export type SubscriptionProjectCommentsUpdatedArgs = { target: ViewerUpdateTrackingTarget; };