diff --git a/packages/frontend-2/lib/common/generated/gql/graphql.ts b/packages/frontend-2/lib/common/generated/gql/graphql.ts index bfca57235..c42086e60 100644 --- a/packages/frontend-2/lib/common/generated/gql/graphql.ts +++ b/packages/frontend-2/lib/common/generated/gql/graphql.ts @@ -182,6 +182,48 @@ export type AuthStrategy = { url: Scalars['String']; }; +export type AutomationFunctionRunStatus = { + __typename?: 'AutomationFunctionRunStatus'; + blobs: Array; + contextView?: Maybe; + elapsed: Scalars['Float']; + functionId: Scalars['String']; + objectResults: Scalars['JSONObject']; + resultVersionIds: Array; + runStatus: AutomationRunStatus; + statusMessage?: Maybe; +}; + +export type AutomationMutations = { + __typename?: 'AutomationMutations'; + create: Scalars['Boolean']; + functionRunStatusReport: Scalars['Boolean']; +}; + + +export type AutomationMutationsCreateArgs = { + input: ModelAutomationCreateInput; +}; + + +export type AutomationMutationsFunctionRunStatusReportArgs = { + input: ModelAutomationRunStatusUpdateInput; +}; + +export enum AutomationRunStatus { + Failed = 'FAILED', + Initializing = 'INITIALIZING', + Running = 'RUNNING', + Succeeded = 'SUCCEEDED' +} + +export type AutomationsStatus = { + __typename?: 'AutomationsStatus'; + automationRuns: Array; + status: AutomationRunStatus; + statusMessage?: Maybe; +}; + export type BlobMetadata = { __typename?: 'BlobMetadata'; createdAt: Scalars['DateTime']; @@ -588,6 +630,17 @@ export type FileUpload = { userId: Scalars['String']; }; +export type FunctionRunStatusInput = { + blobs: Array; + contextView?: InputMaybe; + elapsed: Scalars['Float']; + functionId: Scalars['String']; + objectResults: Scalars['JSONObject']; + resultVersionIds: Array; + runStatus: AutomationRunStatus; + statusMessage?: InputMaybe; +}; + export type LegacyCommentViewerData = { __typename?: 'LegacyCommentViewerData'; /** @@ -678,6 +731,7 @@ export type LimitedUserTimelineArgs = { export type Model = { __typename?: 'Model'; author: LimitedUser; + automationStatus?: Maybe; /** Return a model tree of children */ childrenTree: Array; /** All comment threads in this model */ @@ -720,6 +774,54 @@ export type ModelVersionsArgs = { limit?: Scalars['Int']; }; +export type ModelAutomation = { + __typename?: 'ModelAutomation'; + automationId: Scalars['String']; + automationName: Scalars['String']; + automationRevisionId: Scalars['String']; + createdAt: Scalars['DateTime']; + runs: ModelAutomationRunsCollection; +}; + + +export type ModelAutomationRunsArgs = { + cursor?: InputMaybe; + limit?: Scalars['Int']; +}; + +export type ModelAutomationCreateInput = { + automationId: Scalars['String']; + automationName: Scalars['String']; + automationRevisionId: Scalars['String']; + modelId: Scalars['String']; + projectId: Scalars['String']; +}; + +export type ModelAutomationRun = { + __typename?: 'ModelAutomationRun'; + automationRunId: Scalars['String']; + createdAt: Scalars['DateTime']; + functionRunStatuses: Array; + runStatus: AutomationRunStatus; + updatedAt: Scalars['DateTime']; + versionId: Scalars['String']; +}; + +export type ModelAutomationRunStatusUpdateInput = { + automationId: Scalars['String']; + automationRevisionId: Scalars['String']; + automationRunId: Scalars['String']; + functionRunStatuses: Array; + versionId: Scalars['String']; +}; + +export type ModelAutomationRunsCollection = { + __typename?: 'ModelAutomationRunsCollection'; + cursor?: Maybe; + items: Array; + totalCount: Scalars['Int']; +}; + export type ModelCollection = { __typename?: 'ModelCollection'; cursor?: Maybe; @@ -805,6 +907,7 @@ export type Mutation = { appRevokeAccess?: Maybe; /** Update an existing third party application. **Note: This will invalidate all existing tokens, refresh tokens and access codes and will require existing users to re-authorize it.** */ appUpdate: Scalars['Boolean']; + automationMutations: AutomationMutations; branchCreate: Scalars['String']; branchDelete: Scalars['Boolean']; branchUpdate: Scalars['Boolean']; @@ -2554,6 +2657,7 @@ export type UserUpdateInput = { export type Version = { __typename?: 'Version'; authorUser?: Maybe; + automationStatus?: Maybe; /** All comment threads in this version */ commentThreads: CommentCollection; createdAt: Scalars['DateTime']; diff --git a/packages/server/assets/automations/typedefs/automation.graphql b/packages/server/assets/automations/typedefs/automation.graphql new file mode 100644 index 000000000..6ee41a008 --- /dev/null +++ b/packages/server/assets/automations/typedefs/automation.graphql @@ -0,0 +1,112 @@ +extend type Model { + automationStatus: AutomationsStatus +} + +type AutomationsStatus { + status: AutomationRunStatus! + statusMessage: String + automationRuns: [ModelAutomationRun!]! +} + +extend type Version { + automationStatus: AutomationsStatus +} + +type ModelAutomation { + automationName: String! + automationId: String! + automationRevisionId: String! + createdAt: DateTime! + runs(cursor: String, limit: Int! = 25): ModelAutomationRunsCollection! +} + +type ModelAutomationRunsCollection { + totalCount: Int! + cursor: String + items: [ModelAutomationRun!]! +} + +type ModelAutomationRun { + versionId: String! + # automation: ModelAutomation! + createdAt: DateTime! + updatedAt: DateTime! + automationRunId: String! + functionRunStatuses: [AutomationFunctionRunStatus!]! + runStatus: AutomationRunStatus! +} + +type AutomationFunctionRunStatus { + functionId: String! + elapsed: Float! + runStatus: AutomationRunStatus! + # this is a link to a viewer page with potentially composite + contextView: String + # TODO: remove this from the schema and add it as a type override + resultVersionIds: [String!]! + # resultVersions: [Version!]! + + blobs: [String!]! + statusMessage: String + # its a {version: string; objectResults: Record>} + # where the record key is the object id + objectResults: JSONObject! +} + +extend type Mutation { + automationMutations: AutomationMutations! +} + +type AutomationMutations { + functionRunStatusReport(input: ModelAutomationRunStatusUpdateInput!): Boolean! + @hasServerRole(role: SERVER_USER) + # @hasStreamRole(role: STREAM_OWNER) # who can do this? + # @hasScope(scope: "automation:result") # TODO: add the scope constant + create(input: ModelAutomationCreateInput!): Boolean! @hasServerRole(role: SERVER_USER) + # @hasStreamRole(role: STREAM_OWNER) # TODO: check for stream ownership in the service + # @hasScope(scope: "automation:create") # TODO: add the scope constant +} + +input ModelAutomationCreateInput { + projectId: String! + modelId: String! + automationName: String! + automationId: String! + automationRevisionId: String! +} + +input ModelAutomationRunStatusUpdateInput { + versionId: String! + automationId: String! + automationRevisionId: String! + automationRunId: String! + # functionReleaseId: String! + functionRunStatuses: [FunctionRunStatusInput!]! +} + +enum AutomationRunStatus { + INITIALIZING + RUNNING + SUCCEEDED + FAILED + # TIMEOUT needs to be handled as an error +} + +input FunctionRunStatusInput { + # we cannot strictly require these values, cause local testers of function, wont have it... + # Or should we? + functionId: String! + elapsed: Float! + runStatus: AutomationRunStatus! + contextView: String + resultVersionIds: [String!]! + blobs: [String!]! + statusMessage: String + # its a Record + # where the record key is the object id + objectResults: JSONObject! +} + +# extend type Subscription { +# automationRunStatusUpdated() +# } diff --git a/packages/server/codegen.yml b/packages/server/codegen.yml index 93adf669c..76a7a24a0 100644 --- a/packages/server/codegen.yml +++ b/packages/server/codegen.yml @@ -31,6 +31,8 @@ generates: Comment: '@/modules/comments/helpers/graphTypes#CommentGraphQLReturn' PendingStreamCollaborator: '@/modules/serverinvites/helpers/graphTypes#PendingStreamCollaboratorGraphQLReturn' FileUpload: '@/modules/fileuploads/helpers/types#FileUploadGraphQLReturn' + AutomationMutations: '@/modules/core/helpers/graphTypes#MutationsObjectGraphQLReturn' + # ModelAutomationRun: '@/modules/automations/helpers/graphTypes#ModelAutomationRunGraphQLReturn' modules/cross-server-sync/graph/generated/graphql.ts: plugins: - 'typescript' diff --git a/packages/server/modules/automations/graph/resolvers/automations.ts b/packages/server/modules/automations/graph/resolvers/automations.ts new file mode 100644 index 000000000..386cb49a3 --- /dev/null +++ b/packages/server/modules/automations/graph/resolvers/automations.ts @@ -0,0 +1,53 @@ +import { + createModelAutomation, + getAutomationsStatus, + upsertModelAutomationRunResult +} from '@/modules/automations/services/automations' +import { Resolvers } from '@/modules/core/graph/generated/graphql' + +export = { + Model: { + async automationStatus(parent, _, ctx) { + // we're using the branch model name still? + const modelId = parent.name + const projectId = parent.streamId + const latestCommit = await ctx.loaders.branches.getLatestCommit.load(parent.id) + // if the model has no versions, no automations could have run + if (!latestCommit) return null + return await getAutomationsStatus({ + projectId, + modelId, + versionId: latestCommit.id + }) + } + }, + Version: { + async automationStatus(parent, _, ctx) { + const versionId = parent.id + const branch = await ctx.loaders.commits.getCommitBranch.load(versionId) + if (!branch) throw Error('Very bad version Id') + const projectId = branch.streamId + // yes, the name, cause of webhooks. + const modelId = branch.name + return await getAutomationsStatus({ + projectId, + modelId, + versionId + }) + } + }, + Mutation: { + automationMutations: () => ({}) + }, + AutomationMutations: { + async create(_, args, context) { + await createModelAutomation(args.input, context.userId) + return true + }, + async functionRunStatusReport(_, args, context) { + const { userId } = context + await upsertModelAutomationRunResult({ userId, input: args.input }) + return true + } + } +} as Resolvers diff --git a/packages/server/modules/automations/helpers/graphTypes.ts b/packages/server/modules/automations/helpers/graphTypes.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/server/modules/automations/helpers/types.ts b/packages/server/modules/automations/helpers/types.ts new file mode 100644 index 000000000..e298cebf6 --- /dev/null +++ b/packages/server/modules/automations/helpers/types.ts @@ -0,0 +1,54 @@ +import { AutomationRunStatus } from '@/modules/core/graph/generated/graphql' +import { z } from 'zod' + +export type ModelAutomation = { + projectId: string + modelId: string + automationId: string + createdAt: Date + automationRevisionId: string + automationName: string +} + +export const SupportedObjectResultsVersions = ['23.09'] as const + +export const ObjectResultLevel = ['INFO', 'WARNING', 'ERROR'] as const + +export const ObjectResultLevelEnum = z.enum(ObjectResultLevel) + +export const ObjectResultValuesSchema = z.object({ + level: z.enum(['INFO', 'WARNING', 'ERROR']), + statusMessage: z.string() +}) + +export const ObjectResultsSchema = z.object({ + version: z.enum(SupportedObjectResultsVersions), + values: z.record(z.string(), ObjectResultValuesSchema.array()) +}) + +export type ObjectResults = z.infer + +export const FunctionRunStatusSchema = z.object({ + functionId: z.string().nonempty(), + elapsed: z.number(), + runStatus: z.nativeEnum(AutomationRunStatus), + contextView: z.string().nullable().default(null), + resultVersionIds: z.string().array(), + blobs: z.string().array(), + statusMessage: z.string().nullable().default(null), + objectResults: ObjectResultsSchema +}) + +export type FunctionRunStatus = z.infer + +export const AutomationRunSchema = z.object({ + automationId: z.string().nonempty(), + automationRevisionId: z.string().nonempty(), + automationRunId: z.string().nonempty(), + versionId: z.string().nonempty(), + createdAt: z.date(), + updatedAt: z.date(), + functionRunStatuses: z.array(FunctionRunStatusSchema) +}) + +export type AutomationRun = z.infer diff --git a/packages/server/modules/automations/index.ts b/packages/server/modules/automations/index.ts new file mode 100644 index 000000000..05d3790c5 --- /dev/null +++ b/packages/server/modules/automations/index.ts @@ -0,0 +1,10 @@ +import { moduleLogger } from '@/logging/logging' +import { SpeckleModule } from '@/modules/shared/helpers/typeHelper' + +const automationModule: SpeckleModule = { + init() { + moduleLogger.info('🤖 Init automations module') + } +} + +export = automationModule diff --git a/packages/server/modules/automations/migrations/20230905162038_automations.ts b/packages/server/modules/automations/migrations/20230905162038_automations.ts new file mode 100644 index 000000000..1d74ed0e6 --- /dev/null +++ b/packages/server/modules/automations/migrations/20230905162038_automations.ts @@ -0,0 +1,51 @@ +import { Knex } from 'knex' + +const AUTOMATIONS_TABLE_NAME = 'automations' +const AUTOMATION_RUNS_TABLE_NAME = 'automation_runs' + +const AUTOMATION_ID = 'automationId' +const AUTOMATION_REVISION_ID = 'automationRevisionId' + +export async function up(knex: Knex): Promise { + await knex.schema.createTable(AUTOMATIONS_TABLE_NAME, (table) => { + table.string(AUTOMATION_ID).notNullable + table.string(AUTOMATION_REVISION_ID).notNullable() + table.string('automationName').notNullable() + table.string('projectId').notNullable().references('id').inTable('streams') + table.string('modelId').notNullable() + table + .timestamp('createdAt', { precision: 3, useTz: true }) + .defaultTo(knex.fn.now()) + .notNullable() + table.primary([AUTOMATION_ID, AUTOMATION_REVISION_ID]) + }) + await knex.schema.createTable(AUTOMATION_RUNS_TABLE_NAME, (table) => { + table.string(AUTOMATION_ID).notNullable() + table.string(AUTOMATION_REVISION_ID).notNullable() + table + .string('versionId') + .references('id') + .inTable('commits') + .notNullable() + .onDelete('cascade') + table.string('automationRunId').primary() + table + .timestamp('createdAt', { precision: 3, useTz: true }) + .defaultTo(knex.fn.now()) + .notNullable() + table + .timestamp('updatedAt', { precision: 3, useTz: true }) + .defaultTo(knex.fn.now()) + .notNullable() + table.jsonb('data') + table + .foreign([AUTOMATION_ID, AUTOMATION_REVISION_ID]) + .references([AUTOMATION_ID, AUTOMATION_REVISION_ID]) + .inTable(AUTOMATIONS_TABLE_NAME) + }) +} + +export async function down(knex: Knex): Promise { + await knex.schema.dropTableIfExists(AUTOMATIONS_TABLE_NAME) + await knex.schema.dropTableIfExists(AUTOMATION_RUNS_TABLE_NAME) +} diff --git a/packages/server/modules/automations/repositories/automations.ts b/packages/server/modules/automations/repositories/automations.ts new file mode 100644 index 000000000..987949dca --- /dev/null +++ b/packages/server/modules/automations/repositories/automations.ts @@ -0,0 +1,63 @@ +import knex from '@/db/knex' +import { ModelAutomation, AutomationRun } from '@/modules/automations/helpers/types' + +const Automations = () => knex('automations') +const AutomationRuns = () => knex('automation_runs') + +export const storeModelAutomation = async (automation: ModelAutomation) => { + await Automations().insert(automation) +} + +export const getModelAutomation = async ( + automationId: string +): Promise => { + return await Automations().where({ automationId }).first() +} + +export const upsertAutomationRunData = async (automationRun: AutomationRun) => { + const insertModel = { + automationId: automationRun.automationId, + automationRevisionId: automationRun.automationRevisionId, + automationRunId: automationRun.automationRunId, + versionId: automationRun.versionId, + createdAt: automationRun.createdAt, + updatedAt: automationRun.updatedAt, + data: automationRun + } + await AutomationRuns().insert(insertModel).onConflict('automationRunId').merge() +} + +export const getAutomationRun = async ( + automationRunId: string +): Promise => { + const item = await AutomationRuns().where({ automationRunId }).first() + if (!item) return null + return item.data +} + +export const getLatestAutomationRunsFor = async ({ + projectId, + modelId, + versionId +}: { + projectId: string + modelId: string + versionId: string +}): Promise => { + const runs = await AutomationRuns() + .innerJoin( + 'automations', + 'automation_runs.automationId', + 'automations.automationId' + ) + .where({ projectId }) + .andWhere({ modelId }) + .andWhere({ versionId }) + .distinctOn('automation_runs.automationId') + .orderBy([ + { column: 'automation_runs.automationId' }, + { column: 'automation_runs.createdAt', order: 'desc' } + ]) + + return runs.map((r) => r.data) +} diff --git a/packages/server/modules/automations/services/automations.ts b/packages/server/modules/automations/services/automations.ts new file mode 100644 index 000000000..738670e3b --- /dev/null +++ b/packages/server/modules/automations/services/automations.ts @@ -0,0 +1,167 @@ +import { getStreamBranchByName } from '@/modules/core/repositories/branches' +import { getStream } from '@/modules/core/repositories/streams' +import { Roles } from '@speckle/shared' +import { + getAutomationRun, + getLatestAutomationRunsFor, + getModelAutomation, + storeModelAutomation +} from '@/modules/automations/repositories/automations' +import _ from 'lodash' +import { + ModelAutomationCreateInput, + ModelAutomationRunStatusUpdateInput, + AutomationRunStatus, + AutomationsStatus +} from '@/modules/core/graph/generated/graphql' +import { upsertAutomationRunData } from '@/modules/automations/repositories/automations' +import { AutomationRun, AutomationRunSchema } from '../helpers/types' +import { ForbiddenError, BadRequestError } from '@/modules/shared/errors' + +export const createModelAutomation = async ( + automation: ModelAutomationCreateInput, + userId?: string | undefined +) => { + // stream acl for user + const stream = await getStream({ userId, streamId: automation.projectId }) + if (!stream) throw new BadRequestError('400 invalid projectId') + if (stream.role !== Roles.Stream.Owner) + throw new ForbiddenError('Only project owners are allowed.') + // TODO: once webhooks are migrated to FE2 terms, we need to get the branch model by id + // const branch = await getBranchById() + const branch = await getStreamBranchByName(automation.projectId, automation.modelId) + if (!branch) throw new BadRequestError('400 invalid modelId') + const insertModel = { ...automation, createdAt: new Date() } + await storeModelAutomation(insertModel) +} + +export async function upsertModelAutomationRunResult({ + userId, + input +}: { + userId: string | null | undefined + input: ModelAutomationRunStatusUpdateInput +}) { + // 1. get the automation from the DB + const automation = await getModelAutomation(input.automationId) + // 2. authz the current user on the automation + const stream = await getStream({ + userId: userId || undefined, + streamId: automation.projectId + }) + if (!stream) throw new BadRequestError('400 invalid projectId') + if (stream.role !== Roles.Stream.Owner) + throw new ForbiddenError('Only project owners are allowed') + // 3. store the result of the run, if it already exists, patch it + const maybeAutomationRun = await getAutomationRun(input.automationRunId) + const insertModel = AutomationRunSchema.parse({ + ...input, + createdAt: new Date(), + updatedAt: new Date() + }) + + if (maybeAutomationRun) { + // some bits we do not allow overriding + insertModel.createdAt = maybeAutomationRun.createdAt + insertModel.versionId = maybeAutomationRun.versionId + insertModel.automationId = maybeAutomationRun.automationId + insertModel.automationRevisionId = maybeAutomationRun.automationRevisionId + + // if the function run status is not in the update, add it from the DB to not loose its data + maybeAutomationRun.functionRunStatuses.map((functionRunStatus) => { + if ( + !insertModel.functionRunStatuses.some( + (frs) => frs.functionId === functionRunStatus.functionId + ) + ) + insertModel.functionRunStatuses.push(functionRunStatus) + }) + } + await upsertAutomationRunData(insertModel) + // 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' }) +} + +const anyFunctionRunsHaveStatus = (ar: AutomationRun, status: AutomationRunStatus) => + ar.functionRunStatuses.some((st) => st.runStatus === status) + +const anyFunctionRunsHaveFailed = (ar: AutomationRun): boolean => + anyFunctionRunsHaveStatus(ar, AutomationRunStatus.Failed) + +const anyFunctionRunsRunning = (ar: AutomationRun): boolean => + anyFunctionRunsHaveStatus(ar, AutomationRunStatus.Running) + +const anyFunctionRunsInitializing = (ar: AutomationRun): boolean => + anyFunctionRunsHaveStatus(ar, AutomationRunStatus.Initializing) + +export const getAutomationsStatus = async ({ + projectId, + modelId, + versionId +}: { + projectId: string + modelId: string + versionId: string +}): Promise => { + const automationRuns = await getLatestAutomationRunsFor({ + projectId, + modelId, + versionId + }) + // automation is registered, but no run status have been reported + if (!automationRuns.length) return null + + const modelAutomationRuns = automationRuns.map((ar) => { + let status: AutomationRunStatus = AutomationRunStatus.Succeeded + if (anyFunctionRunsHaveFailed(ar)) { + status = AutomationRunStatus.Failed + } else if (anyFunctionRunsRunning(ar)) { + status = AutomationRunStatus.Running + } else if (anyFunctionRunsInitializing(ar)) { + status = AutomationRunStatus.Initializing + } + return { ..._.cloneDeep(ar), runStatus: status } + }) + + const failedAutomations = modelAutomationRuns.filter( + (a) => a.runStatus === AutomationRunStatus.Failed + ) + + const runningAutomations = modelAutomationRuns.filter( + (a) => a.runStatus === AutomationRunStatus.Running + ) + const initializingAutomations = modelAutomationRuns.filter( + (a) => a.runStatus === AutomationRunStatus.Initializing + ) + + let status = AutomationRunStatus.Succeeded + let statusMessage = 'All automations have succeeded' + + if (failedAutomations.length) { + status = AutomationRunStatus.Failed + statusMessage = 'Some automations have failed:' + for (const fa of failedAutomations) { + for (const functionRunStatus of fa.functionRunStatuses) { + if (functionRunStatus.runStatus === AutomationRunStatus.Failed) + statusMessage += `\n${functionRunStatus.statusMessage}` + } + } + } else if (runningAutomations.length) { + status = AutomationRunStatus.Running + } else if (initializingAutomations.length) { + status = AutomationRunStatus.Initializing + } + return { + status: status as AutomationRunStatus, + automationRuns: modelAutomationRuns, + statusMessage + } +} diff --git a/packages/server/modules/core/graph/generated/graphql.ts b/packages/server/modules/core/graph/generated/graphql.ts index bd6d56886..941161da5 100644 --- a/packages/server/modules/core/graph/generated/graphql.ts +++ b/packages/server/modules/core/graph/generated/graphql.ts @@ -189,6 +189,48 @@ export type AuthStrategy = { url: Scalars['String']; }; +export type AutomationFunctionRunStatus = { + __typename?: 'AutomationFunctionRunStatus'; + blobs: Array; + contextView?: Maybe; + elapsed: Scalars['Float']; + functionId: Scalars['String']; + objectResults: Scalars['JSONObject']; + resultVersionIds: Array; + runStatus: AutomationRunStatus; + statusMessage?: Maybe; +}; + +export type AutomationMutations = { + __typename?: 'AutomationMutations'; + create: Scalars['Boolean']; + functionRunStatusReport: Scalars['Boolean']; +}; + + +export type AutomationMutationsCreateArgs = { + input: ModelAutomationCreateInput; +}; + + +export type AutomationMutationsFunctionRunStatusReportArgs = { + input: ModelAutomationRunStatusUpdateInput; +}; + +export enum AutomationRunStatus { + Failed = 'FAILED', + Initializing = 'INITIALIZING', + Running = 'RUNNING', + Succeeded = 'SUCCEEDED' +} + +export type AutomationsStatus = { + __typename?: 'AutomationsStatus'; + automationRuns: Array; + status: AutomationRunStatus; + statusMessage?: Maybe; +}; + export type BlobMetadata = { __typename?: 'BlobMetadata'; createdAt: Scalars['DateTime']; @@ -600,6 +642,17 @@ export type FileUpload = { userId: Scalars['String']; }; +export type FunctionRunStatusInput = { + blobs: Array; + contextView?: InputMaybe; + elapsed: Scalars['Float']; + functionId: Scalars['String']; + objectResults: Scalars['JSONObject']; + resultVersionIds: Array; + runStatus: AutomationRunStatus; + statusMessage?: InputMaybe; +}; + export type LegacyCommentViewerData = { __typename?: 'LegacyCommentViewerData'; /** @@ -690,6 +743,7 @@ export type LimitedUserTimelineArgs = { export type Model = { __typename?: 'Model'; author: LimitedUser; + automationStatus?: Maybe; /** Return a model tree of children */ childrenTree: Array; /** All comment threads in this model */ @@ -732,6 +786,54 @@ export type ModelVersionsArgs = { limit?: Scalars['Int']; }; +export type ModelAutomation = { + __typename?: 'ModelAutomation'; + automationId: Scalars['String']; + automationName: Scalars['String']; + automationRevisionId: Scalars['String']; + createdAt: Scalars['DateTime']; + runs: ModelAutomationRunsCollection; +}; + + +export type ModelAutomationRunsArgs = { + cursor?: InputMaybe; + limit?: Scalars['Int']; +}; + +export type ModelAutomationCreateInput = { + automationId: Scalars['String']; + automationName: Scalars['String']; + automationRevisionId: Scalars['String']; + modelId: Scalars['String']; + projectId: Scalars['String']; +}; + +export type ModelAutomationRun = { + __typename?: 'ModelAutomationRun'; + automationRunId: Scalars['String']; + createdAt: Scalars['DateTime']; + functionRunStatuses: Array; + runStatus: AutomationRunStatus; + updatedAt: Scalars['DateTime']; + versionId: Scalars['String']; +}; + +export type ModelAutomationRunStatusUpdateInput = { + automationId: Scalars['String']; + automationRevisionId: Scalars['String']; + automationRunId: Scalars['String']; + functionRunStatuses: Array; + versionId: Scalars['String']; +}; + +export type ModelAutomationRunsCollection = { + __typename?: 'ModelAutomationRunsCollection'; + cursor?: Maybe; + items: Array; + totalCount: Scalars['Int']; +}; + export type ModelCollection = { __typename?: 'ModelCollection'; cursor?: Maybe; @@ -817,6 +919,7 @@ export type Mutation = { appRevokeAccess?: Maybe; /** Update an existing third party application. **Note: This will invalidate all existing tokens, refresh tokens and access codes and will require existing users to re-authorize it.** */ appUpdate: Scalars['Boolean']; + automationMutations: AutomationMutations; branchCreate: Scalars['String']; branchDelete: Scalars['Boolean']; branchUpdate: Scalars['Boolean']; @@ -2566,6 +2669,7 @@ export type UserUpdateInput = { export type Version = { __typename?: 'Version'; authorUser?: Maybe; + automationStatus?: Maybe; /** All comment threads in this version */ commentThreads: CommentCollection; createdAt: Scalars['DateTime']; @@ -2816,6 +2920,10 @@ export type ResolversTypes = { AppCreateInput: AppCreateInput; AppUpdateInput: AppUpdateInput; AuthStrategy: ResolverTypeWrapper; + AutomationFunctionRunStatus: ResolverTypeWrapper; + AutomationMutations: ResolverTypeWrapper; + AutomationRunStatus: AutomationRunStatus; + AutomationsStatus: ResolverTypeWrapper; BigInt: ResolverTypeWrapper; BlobMetadata: ResolverTypeWrapper; BlobMetadataCollection: ResolverTypeWrapper; @@ -2856,12 +2964,18 @@ export type ResolversTypes = { EmailAddress: ResolverTypeWrapper; FileUpload: ResolverTypeWrapper; Float: ResolverTypeWrapper; + FunctionRunStatusInput: FunctionRunStatusInput; ID: ResolverTypeWrapper; Int: ResolverTypeWrapper; JSONObject: ResolverTypeWrapper; LegacyCommentViewerData: ResolverTypeWrapper; LimitedUser: ResolverTypeWrapper; Model: ResolverTypeWrapper; + ModelAutomation: ResolverTypeWrapper; + ModelAutomationCreateInput: ModelAutomationCreateInput; + ModelAutomationRun: ResolverTypeWrapper; + ModelAutomationRunStatusUpdateInput: ModelAutomationRunStatusUpdateInput; + ModelAutomationRunsCollection: ResolverTypeWrapper; ModelCollection: ResolverTypeWrapper & { items: Array }>; ModelMutations: ResolverTypeWrapper; ModelVersionsFilter: ModelVersionsFilter; @@ -2980,6 +3094,9 @@ export type ResolversParentTypes = { AppCreateInput: AppCreateInput; AppUpdateInput: AppUpdateInput; AuthStrategy: AuthStrategy; + AutomationFunctionRunStatus: AutomationFunctionRunStatus; + AutomationMutations: MutationsObjectGraphQLReturn; + AutomationsStatus: AutomationsStatus; BigInt: Scalars['BigInt']; BlobMetadata: BlobMetadata; BlobMetadataCollection: BlobMetadataCollection; @@ -3019,12 +3136,18 @@ export type ResolversParentTypes = { EmailAddress: Scalars['EmailAddress']; FileUpload: FileUploadGraphQLReturn; Float: Scalars['Float']; + FunctionRunStatusInput: FunctionRunStatusInput; ID: Scalars['ID']; Int: Scalars['Int']; JSONObject: Scalars['JSONObject']; LegacyCommentViewerData: LegacyCommentViewerData; LimitedUser: LimitedUserGraphQLReturn; Model: ModelGraphQLReturn; + ModelAutomation: ModelAutomation; + ModelAutomationCreateInput: ModelAutomationCreateInput; + ModelAutomationRun: ModelAutomationRun; + ModelAutomationRunStatusUpdateInput: ModelAutomationRunStatusUpdateInput; + ModelAutomationRunsCollection: ModelAutomationRunsCollection; ModelCollection: Omit & { items: Array }; ModelMutations: MutationsObjectGraphQLReturn; ModelVersionsFilter: ModelVersionsFilter; @@ -3240,6 +3363,31 @@ export type AuthStrategyResolvers; }; +export type AutomationFunctionRunStatusResolvers = { + blobs?: Resolver, ParentType, ContextType>; + contextView?: Resolver, ParentType, ContextType>; + elapsed?: Resolver; + functionId?: Resolver; + objectResults?: Resolver; + resultVersionIds?: Resolver, ParentType, ContextType>; + runStatus?: Resolver; + statusMessage?: Resolver, ParentType, ContextType>; + __isTypeOf?: IsTypeOfResolverFn; +}; + +export type AutomationMutationsResolvers = { + create?: Resolver>; + functionRunStatusReport?: Resolver>; + __isTypeOf?: IsTypeOfResolverFn; +}; + +export type AutomationsStatusResolvers = { + automationRuns?: Resolver, ParentType, ContextType>; + status?: Resolver; + statusMessage?: Resolver, ParentType, ContextType>; + __isTypeOf?: IsTypeOfResolverFn; +}; + export interface BigIntScalarConfig extends GraphQLScalarTypeConfig { name: 'BigInt'; } @@ -3440,6 +3588,7 @@ export type LimitedUserResolvers = { author?: Resolver; + automationStatus?: Resolver, ParentType, ContextType>; childrenTree?: Resolver, ParentType, ContextType>; commentThreads?: Resolver>; createdAt?: Resolver; @@ -3455,6 +3604,32 @@ export type ModelResolvers; }; +export type ModelAutomationResolvers = { + automationId?: Resolver; + automationName?: Resolver; + automationRevisionId?: Resolver; + createdAt?: Resolver; + runs?: Resolver>; + __isTypeOf?: IsTypeOfResolverFn; +}; + +export type ModelAutomationRunResolvers = { + automationRunId?: Resolver; + createdAt?: Resolver; + functionRunStatuses?: Resolver, ParentType, ContextType>; + runStatus?: Resolver; + updatedAt?: Resolver; + versionId?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + +export type ModelAutomationRunsCollectionResolvers = { + cursor?: Resolver, ParentType, ContextType>; + items?: Resolver, ParentType, ContextType>; + totalCount?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + export type ModelCollectionResolvers = { cursor?: Resolver, ParentType, ContextType>; items?: Resolver, ParentType, ContextType>; @@ -3497,6 +3672,7 @@ export type MutationResolvers>; appRevokeAccess?: Resolver, ParentType, ContextType, RequireFields>; appUpdate?: Resolver>; + automationMutations?: Resolver; branchCreate?: Resolver>; branchDelete?: Resolver>; branchUpdate?: Resolver>; @@ -3974,6 +4150,7 @@ export type UserSearchResultCollectionResolvers = { authorUser?: Resolver, ParentType, ContextType>; + automationStatus?: Resolver, ParentType, ContextType>; commentThreads?: Resolver>; createdAt?: Resolver; id?: Resolver; @@ -4069,6 +4246,9 @@ export type Resolvers = { ApiToken?: ApiTokenResolvers; AppAuthor?: AppAuthorResolvers; AuthStrategy?: AuthStrategyResolvers; + AutomationFunctionRunStatus?: AutomationFunctionRunStatusResolvers; + AutomationMutations?: AutomationMutationsResolvers; + AutomationsStatus?: AutomationsStatusResolvers; BigInt?: GraphQLScalarType; BlobMetadata?: BlobMetadataResolvers; BlobMetadataCollection?: BlobMetadataCollectionResolvers; @@ -4090,6 +4270,9 @@ export type Resolvers = { LegacyCommentViewerData?: LegacyCommentViewerDataResolvers; LimitedUser?: LimitedUserResolvers; Model?: ModelResolvers; + ModelAutomation?: ModelAutomationResolvers; + ModelAutomationRun?: ModelAutomationRunResolvers; + ModelAutomationRunsCollection?: ModelAutomationRunsCollectionResolvers; ModelCollection?: ModelCollectionResolvers; ModelMutations?: ModelMutationsResolvers; ModelsTreeItem?: ModelsTreeItemResolvers; 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 460f15ced..69e616352 100644 --- a/packages/server/modules/cross-server-sync/graph/generated/graphql.ts +++ b/packages/server/modules/cross-server-sync/graph/generated/graphql.ts @@ -180,6 +180,48 @@ export type AuthStrategy = { url: Scalars['String']; }; +export type AutomationFunctionRunStatus = { + __typename?: 'AutomationFunctionRunStatus'; + blobs: Array; + contextView?: Maybe; + elapsed: Scalars['Float']; + functionId: Scalars['String']; + objectResults: Scalars['JSONObject']; + resultVersionIds: Array; + runStatus: AutomationRunStatus; + statusMessage?: Maybe; +}; + +export type AutomationMutations = { + __typename?: 'AutomationMutations'; + create: Scalars['Boolean']; + functionRunStatusReport: Scalars['Boolean']; +}; + + +export type AutomationMutationsCreateArgs = { + input: ModelAutomationCreateInput; +}; + + +export type AutomationMutationsFunctionRunStatusReportArgs = { + input: ModelAutomationRunStatusUpdateInput; +}; + +export enum AutomationRunStatus { + Failed = 'FAILED', + Initializing = 'INITIALIZING', + Running = 'RUNNING', + Succeeded = 'SUCCEEDED' +} + +export type AutomationsStatus = { + __typename?: 'AutomationsStatus'; + automationRuns: Array; + status: AutomationRunStatus; + statusMessage?: Maybe; +}; + export type BlobMetadata = { __typename?: 'BlobMetadata'; createdAt: Scalars['DateTime']; @@ -591,6 +633,17 @@ export type FileUpload = { userId: Scalars['String']; }; +export type FunctionRunStatusInput = { + blobs: Array; + contextView?: InputMaybe; + elapsed: Scalars['Float']; + functionId: Scalars['String']; + objectResults: Scalars['JSONObject']; + resultVersionIds: Array; + runStatus: AutomationRunStatus; + statusMessage?: InputMaybe; +}; + export type LegacyCommentViewerData = { __typename?: 'LegacyCommentViewerData'; /** @@ -681,6 +734,7 @@ export type LimitedUserTimelineArgs = { export type Model = { __typename?: 'Model'; author: LimitedUser; + automationStatus?: Maybe; /** Return a model tree of children */ childrenTree: Array; /** All comment threads in this model */ @@ -723,6 +777,54 @@ export type ModelVersionsArgs = { limit?: Scalars['Int']; }; +export type ModelAutomation = { + __typename?: 'ModelAutomation'; + automationId: Scalars['String']; + automationName: Scalars['String']; + automationRevisionId: Scalars['String']; + createdAt: Scalars['DateTime']; + runs: ModelAutomationRunsCollection; +}; + + +export type ModelAutomationRunsArgs = { + cursor?: InputMaybe; + limit?: Scalars['Int']; +}; + +export type ModelAutomationCreateInput = { + automationId: Scalars['String']; + automationName: Scalars['String']; + automationRevisionId: Scalars['String']; + modelId: Scalars['String']; + projectId: Scalars['String']; +}; + +export type ModelAutomationRun = { + __typename?: 'ModelAutomationRun'; + automationRunId: Scalars['String']; + createdAt: Scalars['DateTime']; + functionRunStatuses: Array; + runStatus: AutomationRunStatus; + updatedAt: Scalars['DateTime']; + versionId: Scalars['String']; +}; + +export type ModelAutomationRunStatusUpdateInput = { + automationId: Scalars['String']; + automationRevisionId: Scalars['String']; + automationRunId: Scalars['String']; + functionRunStatuses: Array; + versionId: Scalars['String']; +}; + +export type ModelAutomationRunsCollection = { + __typename?: 'ModelAutomationRunsCollection'; + cursor?: Maybe; + items: Array; + totalCount: Scalars['Int']; +}; + export type ModelCollection = { __typename?: 'ModelCollection'; cursor?: Maybe; @@ -808,6 +910,7 @@ export type Mutation = { appRevokeAccess?: Maybe; /** Update an existing third party application. **Note: This will invalidate all existing tokens, refresh tokens and access codes and will require existing users to re-authorize it.** */ appUpdate: Scalars['Boolean']; + automationMutations: AutomationMutations; branchCreate: Scalars['String']; branchDelete: Scalars['Boolean']; branchUpdate: Scalars['Boolean']; @@ -2557,6 +2660,7 @@ export type UserUpdateInput = { export type Version = { __typename?: 'Version'; authorUser?: Maybe; + automationStatus?: Maybe; /** All comment threads in this version */ commentThreads: CommentCollection; createdAt: Scalars['DateTime']; diff --git a/packages/server/modules/index.js b/packages/server/modules/index.js index 0efa451de..d3e5c094f 100644 --- a/packages/server/modules/index.js +++ b/packages/server/modules/index.js @@ -57,7 +57,8 @@ async function getSpeckleModules() { './activitystream', './accessrequests', './webhooks', - './cross-server-sync' + './cross-server-sync', + './automations' ] for (const dir of moduleDirs) { diff --git a/packages/server/package.json b/packages/server/package.json index 53f82afde..e61927c7e 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -100,6 +100,7 @@ "undici": "^5.19.1", "verror": "^1.10.1", "xml-escape": "^1.1.0", + "zod": "^3.22.2", "zxcvbn": "^4.4.2" }, "devDependencies": { diff --git a/packages/server/test/graphql/generated/graphql.ts b/packages/server/test/graphql/generated/graphql.ts index a0c2c6b0d..a9c70086d 100644 --- a/packages/server/test/graphql/generated/graphql.ts +++ b/packages/server/test/graphql/generated/graphql.ts @@ -180,6 +180,48 @@ export type AuthStrategy = { url: Scalars['String']; }; +export type AutomationFunctionRunStatus = { + __typename?: 'AutomationFunctionRunStatus'; + blobs: Array; + contextView?: Maybe; + elapsed: Scalars['Float']; + functionId: Scalars['String']; + objectResults: Scalars['JSONObject']; + resultVersionIds: Array; + runStatus: AutomationRunStatus; + statusMessage?: Maybe; +}; + +export type AutomationMutations = { + __typename?: 'AutomationMutations'; + create: Scalars['Boolean']; + functionRunStatusReport: Scalars['Boolean']; +}; + + +export type AutomationMutationsCreateArgs = { + input: ModelAutomationCreateInput; +}; + + +export type AutomationMutationsFunctionRunStatusReportArgs = { + input: ModelAutomationRunStatusUpdateInput; +}; + +export enum AutomationRunStatus { + Failed = 'FAILED', + Initializing = 'INITIALIZING', + Running = 'RUNNING', + Succeeded = 'SUCCEEDED' +} + +export type AutomationsStatus = { + __typename?: 'AutomationsStatus'; + automationRuns: Array; + status: AutomationRunStatus; + statusMessage?: Maybe; +}; + export type BlobMetadata = { __typename?: 'BlobMetadata'; createdAt: Scalars['DateTime']; @@ -591,6 +633,17 @@ export type FileUpload = { userId: Scalars['String']; }; +export type FunctionRunStatusInput = { + blobs: Array; + contextView?: InputMaybe; + elapsed: Scalars['Float']; + functionId: Scalars['String']; + objectResults: Scalars['JSONObject']; + resultVersionIds: Array; + runStatus: AutomationRunStatus; + statusMessage?: InputMaybe; +}; + export type LegacyCommentViewerData = { __typename?: 'LegacyCommentViewerData'; /** @@ -681,6 +734,7 @@ export type LimitedUserTimelineArgs = { export type Model = { __typename?: 'Model'; author: LimitedUser; + automationStatus?: Maybe; /** Return a model tree of children */ childrenTree: Array; /** All comment threads in this model */ @@ -723,6 +777,54 @@ export type ModelVersionsArgs = { limit?: Scalars['Int']; }; +export type ModelAutomation = { + __typename?: 'ModelAutomation'; + automationId: Scalars['String']; + automationName: Scalars['String']; + automationRevisionId: Scalars['String']; + createdAt: Scalars['DateTime']; + runs: ModelAutomationRunsCollection; +}; + + +export type ModelAutomationRunsArgs = { + cursor?: InputMaybe; + limit?: Scalars['Int']; +}; + +export type ModelAutomationCreateInput = { + automationId: Scalars['String']; + automationName: Scalars['String']; + automationRevisionId: Scalars['String']; + modelId: Scalars['String']; + projectId: Scalars['String']; +}; + +export type ModelAutomationRun = { + __typename?: 'ModelAutomationRun'; + automationRunId: Scalars['String']; + createdAt: Scalars['DateTime']; + functionRunStatuses: Array; + runStatus: AutomationRunStatus; + updatedAt: Scalars['DateTime']; + versionId: Scalars['String']; +}; + +export type ModelAutomationRunStatusUpdateInput = { + automationId: Scalars['String']; + automationRevisionId: Scalars['String']; + automationRunId: Scalars['String']; + functionRunStatuses: Array; + versionId: Scalars['String']; +}; + +export type ModelAutomationRunsCollection = { + __typename?: 'ModelAutomationRunsCollection'; + cursor?: Maybe; + items: Array; + totalCount: Scalars['Int']; +}; + export type ModelCollection = { __typename?: 'ModelCollection'; cursor?: Maybe; @@ -808,6 +910,7 @@ export type Mutation = { appRevokeAccess?: Maybe; /** Update an existing third party application. **Note: This will invalidate all existing tokens, refresh tokens and access codes and will require existing users to re-authorize it.** */ appUpdate: Scalars['Boolean']; + automationMutations: AutomationMutations; branchCreate: Scalars['String']; branchDelete: Scalars['Boolean']; branchUpdate: Scalars['Boolean']; @@ -2557,6 +2660,7 @@ export type UserUpdateInput = { export type Version = { __typename?: 'Version'; authorUser?: Maybe; + automationStatus?: Maybe; /** All comment threads in this version */ commentThreads: CommentCollection; createdAt: Scalars['DateTime']; diff --git a/workspace.code-workspace b/workspace.code-workspace index ed4422463..6a77dcdc4 100644 --- a/workspace.code-workspace +++ b/workspace.code-workspace @@ -86,7 +86,7 @@ }, "files.eol": "\n", "volar.vueserver.maxOldSpaceSize": 4000, - "cSpell.words": ["Bursty", "mjml"], + "cSpell.words": ["Automations", "Bursty", "mjml"], "tailwindCSS.experimental.configFile": { "packages/frontend-2/tailwind.config.mjs": "packages/frontend-2/**" }, @@ -104,7 +104,7 @@ }, "[vue]": { "editor.defaultFormatter": "esbenp.prettier-vscode" - }, + } }, "extensions": { // See https://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations. diff --git a/yarn.lock b/yarn.lock index e79f7e1c6..d4de5473d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12791,6 +12791,7 @@ __metadata: ws: ^7.5.7 xml-escape: ^1.1.0 yargs: ^17.3.1 + zod: ^3.22.2 zxcvbn: ^4.4.2 languageName: unknown linkType: soft @@ -44173,6 +44174,13 @@ __metadata: languageName: node linkType: hard +"zod@npm:^3.22.2": + version: 3.22.2 + resolution: "zod@npm:3.22.2" + checksum: 231e2180c8eabb56e88680d80baff5cf6cbe6d64df3c44c50ebe52f73081ecd0229b1c7215b9552537f537a36d9e36afac2737ddd86dc14e3519bdbc777e82b9 + languageName: node + linkType: hard + "zxcvbn@npm:^4.4.2": version: 4.4.2 resolution: "zxcvbn@npm:4.4.2"