Files
speckle-server/packages/server/modules/cli/commands/download/commit.ts
T
Iain Sproat 444d2ca7dd Structured logging (attempt 2) (#1234)
* Revert "Revert "feat(structured logging): implements structured logging for backend (#1217)" (#1227)"

This reverts commit 63e6581162.

* Use pino-http instead of express pino logger
* Use correct reference to knex and do not instantiate HttpLogger prematurely
* Adds missing dependency for pino to webhook-service
* Do not instantiate middleware when passed to express
* Refactor to move logging into shared
* Copy shared packages into dockerfiles
* Build shared workspace in docker build-stage for fileimport & webhook
2022-12-05 14:49:52 +00:00

294 lines
8.2 KiB
TypeScript

import fetch from 'cross-fetch'
import {
ApolloClient,
InMemoryCache,
NormalizedCacheObject,
gql,
HttpLink,
ApolloQueryResult
} from '@apollo/client/core'
import { CommandModule } from 'yargs'
import { getBaseUrl, getServerVersion } from '@/modules/shared/helpers/envHelper'
import { Commit } from '@/test/graphql/generated/graphql'
import { getStreamBranchByName } from '@/modules/core/repositories/branches'
import { getStream, getStreamCollaborators } from '@/modules/core/repositories/streams'
import { createCommitByBranchId } from '@/modules/core/services/commits'
import { Roles } from '@speckle/shared'
import { addCommitCreatedActivity } from '@/modules/activitystream/services/commitActivity'
import { createObject } from '@/modules/core/services/objects'
import { getObject } from '@/modules/core/repositories/objects'
import ObjectLoader from '@speckle/objectloader'
import { noop } from 'lodash'
import { cliLogger } from '@/logging/logging'
type LocalResources = Awaited<ReturnType<typeof getLocalResources>>
type ParsedCommitUrl = ReturnType<typeof parseCommitUrl>
type GraphQLClient = ApolloClient<NormalizedCacheObject>
type ObjectLoaderObject = Record<string, unknown> & {
id: string
speckle_type: string
totalChildrenCount: number
}
const COMMIT_URL_RGX = /((https?:\/\/)?[\w.]+)\/streams\/([\w]+)\/commits\/([\w]+)/i
const testQuery = gql`
query CommitDownloadTest {
_
}
`
const commitMetadataQuery = gql`
query CommitDownloadMetadata($streamId: String!, $commitId: String!) {
stream(id: $streamId) {
commit(id: $commitId) {
id
referencedObject
authorId
message
createdAt
sourceApplication
totalChildrenCount
parents
}
}
}
`
const assertValidGraphQLResult = (
res: ApolloQueryResult<unknown>,
operationName: string
) => {
if (res.errors?.length) {
throw new Error(
`GQL operation '${operationName}' failed because of errors: ` +
JSON.stringify(res.errors)
)
}
}
const parseCommitUrl = (url: string) => {
const [, origin, , streamId, commitId] = COMMIT_URL_RGX.exec(url) || []
if (!origin || !streamId || !commitId) {
throw new Error("Couldn't parse commit URL! Does it follow the expected format?")
}
return { origin, streamId, commitId }
}
const getLocalResources = async (targetStreamId: string, branchName: string) => {
const targetStream = await getStream({ streamId: targetStreamId })
if (!targetStream) {
throw new Error(`Couldn't find local stream with id ${targetStreamId}`)
}
const targetBranch = await getStreamBranchByName(targetStreamId, branchName)
if (!targetBranch) {
throw new Error(
`Couldn't find local branch ${branchName} in stream ${targetStreamId}`
)
}
const streamOwners = await getStreamCollaborators(targetStreamId, Roles.Stream.Owner)
const owner = streamOwners[0]
return { targetStream, targetBranch, owner }
}
const createApolloClient = async (origin: string): Promise<GraphQLClient> => {
const cache = new InMemoryCache()
const client = new ApolloClient({
link: new HttpLink({ uri: `${origin}/graphql`, fetch }),
cache,
name: 'cli',
version: getServerVersion(),
defaultOptions: {
query: {
fetchPolicy: 'no-cache',
errorPolicy: 'all'
}
}
})
// Test it out
const res = await client.query({
query: testQuery
})
assertValidGraphQLResult(res, 'Target server test query')
if (!res.data?._) {
throw new Error(
"Couldn't construct working Apollo Client, test query failed cause of unexpected response: " +
JSON.stringify(res.data)
)
}
return client
}
const getCommitMetadata = async (client: GraphQLClient, params: ParsedCommitUrl) => {
const { streamId, commitId } = params
const results = await client.query({
query: commitMetadataQuery,
variables: { streamId, commitId }
})
assertValidGraphQLResult(results, 'Commit Metadata Query')
const commit = results.data?.stream?.commit
if (!commit) {
throw new Error('Unexpectedly received invalid commit structure')
}
return commit as Commit
}
const saveNewCommit = async (commit: Commit, localResources: LocalResources) => {
const { targetStream, targetBranch, owner } = localResources
const streamId = targetStream.id
const message = commit.message
const objectId = commit.referencedObject
const parents = commit.parents
const sourceApplication = commit.sourceApplication
const totalChildrenCount = commit.totalChildrenCount
const id = await createCommitByBranchId({
streamId,
branchId: targetBranch.id,
objectId,
authorId: owner.id,
message,
sourceApplication,
totalChildrenCount,
parents
})
await addCommitCreatedActivity({
commitId: id,
streamId,
userId: owner.id,
commit: {
branchName: targetBranch.name,
message,
objectId,
parents,
sourceApplication,
streamId,
totalChildrenCount
},
branchName: targetBranch.name
})
return id
}
const createNewObject = async (
newObject: ObjectLoaderObject,
targetStreamId: string
) => {
if (!newObject) {
cliLogger.error('Encountered falsy object!')
return
}
const newObjectId = await createObject(targetStreamId, {
...newObject,
id: newObject.id,
speckleType: newObject.speckleType || newObject.speckle_type || 'Base'
})
const newRecord = await getObject(newObjectId)
if (!newRecord) {
throw new Error("Unexpected error! Just inserted an object, but can't find it!")
}
return newRecord
}
const loadAllObjectsFromParent = async (params: {
targetStreamId: string
sourceCommit: Commit
parsedCommitUrl: ParsedCommitUrl
}) => {
const {
targetStreamId,
sourceCommit,
parsedCommitUrl: { origin, streamId: sourceStreamId }
} = params
// Initialize ObjectLoader
const objectLoader = new ObjectLoader({
serverUrl: origin,
streamId: sourceStreamId,
objectId: sourceCommit.referencedObject,
options: { fetch, customLogger: noop }
})
// Iterate over all objects and download them into the DB
const totalObjectCount = (sourceCommit.totalChildrenCount || 0) + 1
let processedObjectCount = 1
for await (const obj of objectLoader.getObjectIterator()) {
const typedObj = obj as ObjectLoaderObject
cliLogger.info(
`Processing ${obj.id} - ${processedObjectCount++}/${totalObjectCount}`
)
await createNewObject(typedObj, targetStreamId)
}
}
const command: CommandModule<
unknown,
{ commitUrl: string; targetStreamId: string; branchName: string }
> = {
command: 'commit <commitUrl> <targetStreamId> [branchName]',
describe: 'Download a commit from an external Speckle server instance',
builder: {
commitUrl: {
describe:
'Commit URL (e.g. https://speckle.xyz/streams/f0532359ac/commits/98678e2a3d)',
type: 'string'
},
targetStreamId: {
describe: 'ID of the local stream that should receive the commit',
type: 'string'
},
branchName: {
describe: 'Stream branch that should receive the commit',
type: 'string',
default: 'main'
}
},
handler: async (argv) => {
const { commitUrl, targetStreamId, branchName } = argv
cliLogger.info(`Process started at: ${new Date().toISOString()}`)
const localResources = await getLocalResources(targetStreamId, branchName)
cliLogger.info(
`Using local branch ${branchName} of stream ${targetStreamId} to dump the incoming commit`
)
const parsedCommitUrl = parseCommitUrl(commitUrl)
cliLogger.info('Loading the following commit: %s', parsedCommitUrl)
const client = await createApolloClient(parsedCommitUrl.origin)
const commit = await getCommitMetadata(client, parsedCommitUrl)
cliLogger.info('Loaded commit metadata: %s', commit)
const newCommitId = await saveNewCommit(commit, localResources)
cliLogger.info(`Created new local commit: ${newCommitId}`)
cliLogger.info(`Pulling & saving all objects! (${commit.totalChildrenCount})`)
await loadAllObjectsFromParent({
targetStreamId,
sourceCommit: commit,
parsedCommitUrl
})
const linkToNewCommit = `${getBaseUrl()}/streams/${targetStreamId}/commits/${newCommitId}`
cliLogger.info(`All done! Find your commit here: ${linkToNewCommit}`)
}
}
export = command