Merge branch 'main' into iain/preview-service-handle-errors

This commit is contained in:
Iain Sproat
2025-03-28 10:37:19 +00:00
23 changed files with 517 additions and 425 deletions
+8 -1
View File
@@ -522,7 +522,14 @@ jobs:
- run:
name: 'Run tests'
# Extra formatting to get timestamps on each line in CI (for profiling purposes)
command: yarn test:report --color=always | while IFS= read -r line; do echo -e "$(date +%T.%3N) > $line"; done
command: |
GREP_FLAG=""
if [ "$RUN_TESTS_IN_MULTIREGION_MODE" == "true" ]; then
GREP_FLAG="--grep @multiregion"
fi
yarn test:report $GREP_FLAG --color=always | while IFS= read -r line; do echo -e "$(date +%T.%3N) > $line"; done
working_directory: 'packages/server'
no_output_timeout: 30m
+13
View File
@@ -25,6 +25,19 @@
"endpoint": "http://127.0.0.1:9020",
"s3Region": "us-east-1"
}
},
"region2": {
"postgres": {
"connectionUri": "postgresql://speckle:speckle@127.0.0.1:5434/speckle2_test"
},
"blobStorage": {
"accessKey": "minioadmin",
"secretKey": "minioadmin",
"bucket": "speckle-server",
"createBucketIfNotExists": true,
"endpoint": "http://127.0.0.1:9040",
"s3Region": "us-east-1"
}
}
}
}
@@ -1,4 +1,3 @@
import Environment from '@speckle/shared/dist/commonjs/environment/index.js'
import {
initPrometheusMetrics,
metricDuration,
@@ -18,8 +17,6 @@ import { Nullable, Scopes, wait } from '@speckle/shared'
import { Knex } from 'knex'
import { Logger } from 'pino'
const { FF_FILEIMPORT_IFC_DOTNET_ENABLED } = Environment.getFeatureFlags()
const HEALTHCHECK_FILE_PATH = '/tmp/last_successful_query'
const TMP_INPUT_DIR = '/tmp/file_to_import'
@@ -156,27 +153,7 @@ async function doTask(
taskLogger.info('Triggering importer for {fileType}')
if (info.fileType.toLowerCase() === 'ifc') {
if (FF_FILEIMPORT_IFC_DOTNET_ENABLED) {
await runProcessWithTimeout(
taskLogger,
process.env['DOTNET_BINARY_PATH'] || 'dotnet',
[
process.env['IFC_DOTNET_DLL_PATH'] ||
'/speckle-server/packages/fileimport-service/src/ifc-dotnet/ifc-converter.dll',
TMP_FILE_PATH,
TMP_RESULTS_PATH,
info.streamId,
`File upload: ${info.fileName}`,
existingBranch?.id || '',
info.branchName,
regionName
],
{
USER_TOKEN: tempUserToken
},
TIME_LIMIT
)
} else {
if (info.fileName.toLowerCase().endsWith('.legacyimporter.ifc')) {
await runProcessWithTimeout(
taskLogger,
process.env['NODE_BINARY_PATH'] || 'node',
@@ -199,6 +176,26 @@ async function doTask(
},
TIME_LIMIT
)
} else {
await runProcessWithTimeout(
taskLogger,
process.env['DOTNET_BINARY_PATH'] || 'dotnet',
[
process.env['IFC_DOTNET_DLL_PATH'] ||
'/speckle-server/packages/fileimport-service/src/ifc-dotnet/ifc-converter.dll',
TMP_FILE_PATH,
TMP_RESULTS_PATH,
info.streamId,
`File upload: ${info.fileName}`,
existingBranch?.id || '',
info.branchName,
regionName
],
{
USER_TOKEN: tempUserToken
},
TIME_LIMIT
)
}
} else if (info.fileType.toLowerCase() === 'stl') {
await runProcessWithTimeout(
+3
View File
@@ -10,6 +10,9 @@ PORT=3000
CANONICAL_URL="http://127.0.0.1:3000"
SESSION_SECRET="-> FILL IN <-"
# Optional license token for paid features like multiregion
# LICENSE_TOKEN=
# Redis connection: default for local development environment
REDIS_URL="redis://127.0.0.1:6379"
@@ -222,7 +222,7 @@ describe('Core GraphQL Subscriptions (New)', () => {
]
modes.forEach(({ isMultiRegion }) => {
describe(`W/${!isMultiRegion ? 'o' : ''} multiregion`, () => {
describe(`W/${!isMultiRegion ? 'o' : ''} @multiregion`, () => {
const myMainWorkspace: BasicTestWorkspace = {
id: '',
ownerId: '',
@@ -0,0 +1,363 @@
import { db } from '@/db/knex'
import { AutomationRecord, AutomationRunRecord } from '@/modules/automate/helpers/types'
import { CommentRecord } from '@/modules/comments/helpers/types'
import { createRandomEmail } from '@/modules/core/helpers/testHelpers'
import { StreamRecord } from '@/modules/core/helpers/types'
import { getDb } from '@/modules/multiregion/utils/dbSelector'
import {
createWebhookConfigFactory,
createWebhookEventFactory
} from '@/modules/webhooks/repositories/webhooks'
import {
BasicTestWorkspace,
createTestWorkspace
} from '@/modules/workspaces/tests/helpers/creation'
import { BasicTestUser, createTestUser } from '@/test/authHelper'
import {
UpdateProjectRegionDocument,
GetProjectDocument,
GetRegionalProjectModelDocument,
GetRegionalProjectVersionDocument,
GetRegionalProjectObjectDocument,
GetRegionalProjectAutomationDocument,
GetRegionalProjectCommentDocument,
GetRegionalProjectWebhookDocument,
GetRegionalProjectBlobDocument
} from '@/test/graphql/generated/graphql'
import { TestApolloServer, testApolloServer } from '@/test/graphqlHelper'
import {
createTestAutomation,
createTestAutomationRun
} from '@/test/speckle-helpers/automationHelper'
import { createTestBlob } from '@/test/speckle-helpers/blobHelper'
import { BasicTestBranch, createTestBranch } from '@/test/speckle-helpers/branchHelper'
import { createTestComment } from '@/test/speckle-helpers/commentHelper'
import {
BasicTestCommit,
createTestObject,
createTestCommit
} from '@/test/speckle-helpers/commitHelper'
import {
isMultiRegionTestMode,
waitForRegionUser
} from '@/test/speckle-helpers/regions'
import { BasicTestStream, createTestStream } from '@/test/speckle-helpers/streamHelper'
import { retry, Roles } from '@speckle/shared'
import { expect } from 'chai'
import cryptoRandomString from 'crypto-random-string'
import { Knex } from 'knex'
import { SetOptional } from 'type-fest'
const tables = {
projects: (db: Knex) => db.table<StreamRecord>('streams')
}
const assertProjectRegion = async (
projectId: string,
regionKey: string
): Promise<void> => {
const project = await tables.projects(db).select('*').where('id', projectId).first()
if (!project || project.regionKey !== regionKey) {
expect.fail('Project is not in expected region.')
}
}
const ensureProjectRegion = async (
projectId: string,
regionKey: string
): Promise<void> => {
await retry(async () => assertProjectRegion(projectId, regionKey), 30, 500)
}
isMultiRegionTestMode()
? describe('Workspace project region changes @multiregion', () => {
const regionKey1 = 'region1'
const regionKey2 = 'region2'
const adminUser: BasicTestUser = {
id: '',
name: 'John Speckle',
email: createRandomEmail(),
role: Roles.Server.Admin
}
const testWorkspace: SetOptional<BasicTestWorkspace, 'slug'> = {
id: '',
ownerId: '',
name: 'Unlimited Workspace'
}
const testProject: BasicTestStream = {
id: '',
ownerId: '',
name: 'Regional Project',
isPublic: true
}
const testModel: BasicTestBranch = {
id: '',
name: cryptoRandomString({ length: 8 }),
streamId: '',
authorId: ''
}
const testVersion: BasicTestCommit = {
id: '',
objectId: '',
streamId: '',
authorId: ''
}
let testAutomation: AutomationRecord
let testAutomationRun: AutomationRunRecord
let testComment: CommentRecord
let testWebhookId: string
let testBlobId: string
let apollo: TestApolloServer
let sourceRegionDb: Knex
before(async () => {
await createTestUser(adminUser)
await waitForRegionUser(adminUser)
apollo = await testApolloServer({ authUserId: adminUser.id })
sourceRegionDb = await getDb({ regionKey: regionKey1 })
})
beforeEach(async () => {
delete testWorkspace.slug
await createTestWorkspace(testWorkspace, adminUser, {
regionKey: regionKey1,
addPlan: {
name: 'unlimited',
status: 'valid'
}
})
testProject.workspaceId = testWorkspace.id
await createTestStream(testProject, adminUser)
await createTestBranch({
stream: testProject,
branch: testModel,
owner: adminUser
})
testVersion.branchName = testModel.name
testVersion.objectId = await createTestObject({ projectId: testProject.id })
await createTestCommit(testVersion, {
owner: adminUser,
stream: testProject
})
const { automation, revision } = await createTestAutomation({
userId: adminUser.id,
projectId: testProject.id,
revision: {
functionId: cryptoRandomString({ length: 9 }),
functionReleaseId: cryptoRandomString({ length: 9 })
}
})
if (!revision) {
throw new Error('Failed to create automation revision.')
}
testAutomation = automation.automation
const { automationRun } = await createTestAutomationRun({
userId: adminUser.id,
projectId: testProject.id,
automationId: testAutomation.id
})
testAutomationRun = automationRun
testComment = await createTestComment({
userId: adminUser.id,
projectId: testProject.id,
objectId: testVersion.objectId
})
testWebhookId = await createWebhookConfigFactory({ db: sourceRegionDb })({
id: cryptoRandomString({ length: 9 }),
streamId: testProject.id,
url: 'https://example.org',
description: cryptoRandomString({ length: 9 }),
secret: cryptoRandomString({ length: 9 }),
enabled: false,
triggers: ['branch_create']
})
await createWebhookEventFactory({ db: sourceRegionDb })({
id: cryptoRandomString({ length: 9 }),
webhookId: testWebhookId,
payload: cryptoRandomString({ length: 9 })
})
const testBlob = await createTestBlob({
userId: adminUser.id,
projectId: testProject.id
})
testBlobId = testBlob.blobId
await assertProjectRegion(testProject.id, regionKey1)
})
it('moves project record to target regional db', async () => {
const resA = await apollo.execute(UpdateProjectRegionDocument, {
projectId: testProject.id,
regionKey: regionKey2
})
expect(resA).to.not.haveGraphQLErrors()
await ensureProjectRegion(testProject.id, regionKey2)
const resB = await apollo.execute(GetProjectDocument, {
id: testProject.id
})
expect(resB).to.not.haveGraphQLErrors()
expect(resB.data?.project.name).to.equal(testProject.name)
})
it('moves project models to target regional db', async () => {
const resA = await apollo.execute(UpdateProjectRegionDocument, {
projectId: testProject.id,
regionKey: regionKey2
})
expect(resA).to.not.haveGraphQLErrors()
await ensureProjectRegion(testProject.id, regionKey2)
const resB = await apollo.execute(GetRegionalProjectModelDocument, {
projectId: testProject.id,
modelId: testModel.id
})
expect(resB).to.not.haveGraphQLErrors()
expect(resB.data?.project.model.name).to.equal(testModel.name)
})
it('moves project model versions to target regional db', async () => {
const resA = await apollo.execute(UpdateProjectRegionDocument, {
projectId: testProject.id,
regionKey: regionKey2
})
expect(resA).to.not.haveGraphQLErrors()
await ensureProjectRegion(testProject.id, regionKey2)
const resB = await apollo.execute(GetRegionalProjectVersionDocument, {
projectId: testProject.id,
modelId: testModel.id,
versionId: testVersion.id
})
expect(resB).to.not.haveGraphQLErrors()
expect(resB.data?.project.model.version.referencedObject).to.equal(
testVersion.objectId
)
})
it('moves project version objects to target regional db', async () => {
const resA = await apollo.execute(UpdateProjectRegionDocument, {
projectId: testProject.id,
regionKey: regionKey2
})
expect(resA).to.not.haveGraphQLErrors()
await ensureProjectRegion(testProject.id, regionKey2)
const resB = await apollo.execute(GetRegionalProjectObjectDocument, {
projectId: testProject.id,
objectId: testVersion.objectId
})
expect(resB).to.not.haveGraphQLErrors()
expect(resB.data?.project.object).to.not.be.undefined
})
it('moves project automations to target regional db', async () => {
const resA = await apollo.execute(UpdateProjectRegionDocument, {
projectId: testProject.id,
regionKey: regionKey2
})
expect(resA).to.not.haveGraphQLErrors()
await ensureProjectRegion(testProject.id, regionKey2)
const resB = await apollo.execute(GetRegionalProjectAutomationDocument, {
projectId: testProject.id,
automationId: testAutomation.id
})
expect(resB).to.not.haveGraphQLErrors()
expect(resB.data?.project.automation.id).to.equal(testAutomation.id)
expect(resB.data?.project.automation.runs.items.at(0)?.id).to.equal(
testAutomationRun.id
)
expect(
resB.data?.project.automation.runs.items.at(0)?.functionRuns.length
).to.not.equal(0)
})
it('moves project comments to target regional db', async () => {
const resA = await apollo.execute(UpdateProjectRegionDocument, {
projectId: testProject.id,
regionKey: regionKey2
})
expect(resA).to.not.haveGraphQLErrors()
await ensureProjectRegion(testProject.id, regionKey2)
const resB = await apollo.execute(GetRegionalProjectCommentDocument, {
projectId: testProject.id,
commentId: testComment.id
})
expect(resB).to.not.haveGraphQLErrors()
expect(resB.data?.project.comment).to.not.be.undefined
})
it('moves project webhooks to target regional db', async () => {
const resA = await apollo.execute(UpdateProjectRegionDocument, {
projectId: testProject.id,
regionKey: regionKey2
})
expect(resA).to.not.haveGraphQLErrors()
await ensureProjectRegion(testProject.id, regionKey2)
const resB = await apollo.execute(GetRegionalProjectWebhookDocument, {
projectId: testProject.id,
webhookId: testWebhookId
})
expect(resB).to.not.haveGraphQLErrors()
expect(resB.data?.project.webhooks.items.length).to.equal(1)
})
it('moves project files and associated blobs to target regional db and object storage', async () => {
const resA = await apollo.execute(UpdateProjectRegionDocument, {
projectId: testProject.id,
regionKey: regionKey2
})
expect(resA).to.not.haveGraphQLErrors()
await ensureProjectRegion(testProject.id, regionKey2)
const resB = await apollo.execute(GetRegionalProjectBlobDocument, {
projectId: testProject.id,
blobId: testBlobId
})
expect(resB).to.not.haveGraphQLErrors()
expect(resB.data?.project.blob).to.not.be.undefined
})
})
: void 0
@@ -28,7 +28,7 @@ import { expect } from 'chai'
const isEnabled = isMultiRegionEnabled()
isEnabled
? describe('Multi Region Server Settings', () => {
? describe('Multi Region Server Settings @multiregion', () => {
let testAdminUser: BasicTestUser
let testBasicUser: BasicTestUser
let apollo: TestApolloServer
@@ -1,17 +1,8 @@
import { db } from '@/db/knex'
import { AutomationRecord, AutomationRunRecord } from '@/modules/automate/helpers/types'
import { CommentRecord } from '@/modules/comments/helpers/types'
import { AllScopes } from '@/modules/core/helpers/mainConstants'
import { createRandomEmail } from '@/modules/core/helpers/testHelpers'
import { StreamRecord } from '@/modules/core/helpers/types'
import { grantStreamPermissionsFactory } from '@/modules/core/repositories/streams'
import { WorkspaceSeatType } from '@/modules/gatekeeper/domain/billing'
import { getWorkspaceUserSeatsFactory } from '@/modules/gatekeeper/repositories/workspaceSeat'
import { getDb } from '@/modules/multiregion/utils/dbSelector'
import {
createWebhookConfigFactory,
createWebhookEventFactory
} from '@/modules/webhooks/repositories/webhooks'
import { WorkspaceInvalidRoleError } from '@/modules/workspaces/errors/workspace'
import {
assignToWorkspace,
@@ -28,19 +19,10 @@ import {
import {
ActiveUserProjectsWorkspaceDocument,
CreateWorkspaceProjectDocument,
GetProjectDocument,
GetRegionalProjectAutomationDocument,
GetRegionalProjectBlobDocument,
GetRegionalProjectCommentDocument,
GetRegionalProjectModelDocument,
GetRegionalProjectObjectDocument,
GetRegionalProjectVersionDocument,
GetRegionalProjectWebhookDocument,
GetWorkspaceProjectsDocument,
GetWorkspaceTeamDocument,
MoveProjectToWorkspaceDocument,
ProjectUpdateRoleInput,
UpdateProjectRegionDocument,
UpdateProjectRoleDocument,
UpdateWorkspaceProjectRoleDocument
} from '@/test/graphql/generated/graphql'
@@ -50,57 +32,15 @@ import {
TestApolloServer
} from '@/test/graphqlHelper'
import { beforeEachContext } from '@/test/hooks'
import {
createTestAutomation,
createTestAutomationRun
} from '@/test/speckle-helpers/automationHelper'
import { createTestBlob } from '@/test/speckle-helpers/blobHelper'
import { BasicTestBranch, createTestBranch } from '@/test/speckle-helpers/branchHelper'
import { createTestComment } from '@/test/speckle-helpers/commentHelper'
import {
BasicTestCommit,
createTestCommit,
createTestObject
} from '@/test/speckle-helpers/commitHelper'
import {
getMainTestRegionKey,
isMultiRegionTestMode,
waitForRegionUser,
waitForRegionUsers
} from '@/test/speckle-helpers/regions'
import {
addToStream,
BasicTestStream,
createTestStream,
getUserStreamRole
} from '@/test/speckle-helpers/streamHelper'
import { Roles, retry } from '@speckle/shared'
import { Roles } from '@speckle/shared'
import { expect } from 'chai'
import cryptoRandomString from 'crypto-random-string'
import { Knex } from 'knex'
import { SetOptional } from 'type-fest'
const tables = {
projects: (db: Knex) => db.table<StreamRecord>('streams')
}
const assertProjectRegion = async (
projectId: string,
regionKey: string
): Promise<void> => {
const project = await tables.projects(db).select('*').where('id', projectId).first()
if (!project || project.regionKey !== regionKey) {
expect.fail('Project is not in expected region.')
}
}
const ensureProjectRegion = async (
projectId: string,
regionKey: string
): Promise<void> => {
await retry(async () => assertProjectRegion(projectId, regionKey), 20, 10)
}
const grantStreamPermissions = grantStreamPermissionsFactory({ db })
@@ -182,12 +122,6 @@ describe('Workspace project GQL CRUD', () => {
createTestUser(workspaceEditor),
createTestUser(workspaceMemberViewer)
])
await waitForRegionUsers([
serverAdminUser,
workspaceGuest,
workspaceEditor,
workspaceMemberViewer
])
})
describeEach(
@@ -213,8 +147,7 @@ describe('Workspace project GQL CRUD', () => {
await createTestWorkspace(roleWorkspace, serverAdminUser, {
addPlan: oldPlan
? { name: 'business', status: 'valid' }
: { name: 'pro', status: 'valid' },
regionKey: isMultiRegionTestMode() ? getMainTestRegionKey() : undefined
: { name: 'pro', status: 'valid' }
})
roleProject.workspaceId = roleWorkspace.id
@@ -512,296 +445,3 @@ describe('Workspace project GQL CRUD', () => {
})
})
})
// TODO: These are very flaky for some reason
isMultiRegionTestMode()
? describe.skip('Workspace project region changes', () => {
const regionKey1 = 'region1'
const regionKey2 = 'region2'
const adminUser: BasicTestUser = {
id: '',
name: 'John Speckle',
email: createRandomEmail(),
role: Roles.Server.Admin
}
const testWorkspace: SetOptional<BasicTestWorkspace, 'slug'> = {
id: '',
ownerId: '',
name: 'Unlimited Workspace'
}
const testProject: BasicTestStream = {
id: '',
ownerId: '',
name: 'Regional Project',
isPublic: true
}
const testModel: BasicTestBranch = {
id: '',
name: cryptoRandomString({ length: 8 }),
streamId: '',
authorId: ''
}
const testVersion: BasicTestCommit = {
id: '',
objectId: '',
streamId: '',
authorId: ''
}
let testAutomation: AutomationRecord
let testAutomationRun: AutomationRunRecord
let testComment: CommentRecord
let testWebhookId: string
let testBlobId: string
let apollo: TestApolloServer
let sourceRegionDb: Knex
before(async () => {
await createTestUser(adminUser)
await waitForRegionUser(adminUser)
apollo = await testApolloServer({ authUserId: adminUser.id })
sourceRegionDb = await getDb({ regionKey: regionKey1 })
})
beforeEach(async () => {
delete testWorkspace.slug
await createTestWorkspace(testWorkspace, adminUser, {
regionKey: regionKey1,
addPlan: {
name: 'unlimited',
status: 'valid'
}
})
testProject.workspaceId = testWorkspace.id
await createTestStream(testProject, adminUser)
await createTestBranch({
stream: testProject,
branch: testModel,
owner: adminUser
})
testVersion.branchName = testModel.name
testVersion.objectId = await createTestObject({ projectId: testProject.id })
await createTestCommit(testVersion, {
owner: adminUser,
stream: testProject
})
const { automation, revision } = await createTestAutomation({
userId: adminUser.id,
projectId: testProject.id,
revision: {
functionId: cryptoRandomString({ length: 9 }),
functionReleaseId: cryptoRandomString({ length: 9 })
}
})
if (!revision) {
throw new Error('Failed to create automation revision.')
}
testAutomation = automation.automation
const { automationRun } = await createTestAutomationRun({
userId: adminUser.id,
projectId: testProject.id,
automationId: testAutomation.id
})
testAutomationRun = automationRun
testComment = await createTestComment({
userId: adminUser.id,
projectId: testProject.id,
objectId: testVersion.objectId
})
testWebhookId = await createWebhookConfigFactory({ db: sourceRegionDb })({
id: cryptoRandomString({ length: 9 }),
streamId: testProject.id,
url: 'https://example.org',
description: cryptoRandomString({ length: 9 }),
secret: cryptoRandomString({ length: 9 }),
enabled: false,
triggers: ['branch_create']
})
await createWebhookEventFactory({ db: sourceRegionDb })({
id: cryptoRandomString({ length: 9 }),
webhookId: testWebhookId,
payload: cryptoRandomString({ length: 9 })
})
const testBlob = await createTestBlob({
userId: adminUser.id,
projectId: testProject.id
})
testBlobId = testBlob.blobId
await assertProjectRegion(testProject.id, regionKey1)
})
it('moves project record to target regional db', async () => {
const resA = await apollo.execute(UpdateProjectRegionDocument, {
projectId: testProject.id,
regionKey: regionKey2
})
expect(resA).to.not.haveGraphQLErrors()
await ensureProjectRegion(testProject.id, regionKey2)
const resB = await apollo.execute(GetProjectDocument, {
id: testProject.id
})
expect(resB).to.not.haveGraphQLErrors()
expect(resB.data?.project.name).to.equal(testProject.name)
})
it('moves project models to target regional db', async () => {
const resA = await apollo.execute(UpdateProjectRegionDocument, {
projectId: testProject.id,
regionKey: regionKey2
})
expect(resA).to.not.haveGraphQLErrors()
await ensureProjectRegion(testProject.id, regionKey2)
const resB = await apollo.execute(GetRegionalProjectModelDocument, {
projectId: testProject.id,
modelId: testModel.id
})
expect(resB).to.not.haveGraphQLErrors()
expect(resB.data?.project.model.name).to.equal(testModel.name)
})
it('moves project model versions to target regional db', async () => {
const resA = await apollo.execute(UpdateProjectRegionDocument, {
projectId: testProject.id,
regionKey: regionKey2
})
expect(resA).to.not.haveGraphQLErrors()
await ensureProjectRegion(testProject.id, regionKey2)
const resB = await apollo.execute(GetRegionalProjectVersionDocument, {
projectId: testProject.id,
modelId: testModel.id,
versionId: testVersion.id
})
expect(resB).to.not.haveGraphQLErrors()
expect(resB.data?.project.model.version.referencedObject).to.equal(
testVersion.objectId
)
})
it('moves project version objects to target regional db', async () => {
const resA = await apollo.execute(UpdateProjectRegionDocument, {
projectId: testProject.id,
regionKey: regionKey2
})
expect(resA).to.not.haveGraphQLErrors()
await ensureProjectRegion(testProject.id, regionKey2)
const resB = await apollo.execute(GetRegionalProjectObjectDocument, {
projectId: testProject.id,
objectId: testVersion.objectId
})
expect(resB).to.not.haveGraphQLErrors()
expect(resB.data?.project.object).to.not.be.undefined
})
it('moves project automations to target regional db', async () => {
const resA = await apollo.execute(UpdateProjectRegionDocument, {
projectId: testProject.id,
regionKey: regionKey2
})
expect(resA).to.not.haveGraphQLErrors()
await ensureProjectRegion(testProject.id, regionKey2)
const resB = await apollo.execute(GetRegionalProjectAutomationDocument, {
projectId: testProject.id,
automationId: testAutomation.id
})
expect(resB).to.not.haveGraphQLErrors()
expect(resB.data?.project.automation.id).to.equal(testAutomation.id)
expect(resB.data?.project.automation.runs.items.at(0)?.id).to.equal(
testAutomationRun.id
)
expect(
resB.data?.project.automation.runs.items.at(0)?.functionRuns.length
).to.not.equal(0)
})
it('moves project comments to target regional db', async () => {
const resA = await apollo.execute(UpdateProjectRegionDocument, {
projectId: testProject.id,
regionKey: regionKey2
})
expect(resA).to.not.haveGraphQLErrors()
await ensureProjectRegion(testProject.id, regionKey2)
const resB = await apollo.execute(GetRegionalProjectCommentDocument, {
projectId: testProject.id,
commentId: testComment.id
})
expect(resB).to.not.haveGraphQLErrors()
expect(resB.data?.project.comment).to.not.be.undefined
})
it('moves project webhooks to target regional db', async () => {
const resA = await apollo.execute(UpdateProjectRegionDocument, {
projectId: testProject.id,
regionKey: regionKey2
})
expect(resA).to.not.haveGraphQLErrors()
await ensureProjectRegion(testProject.id, regionKey2)
const resB = await apollo.execute(GetRegionalProjectWebhookDocument, {
projectId: testProject.id,
webhookId: testWebhookId
})
expect(resB).to.not.haveGraphQLErrors()
expect(resB.data?.project.webhooks.items.length).to.equal(1)
})
it('moves project files and associated blobs to target regional db and object storage', async () => {
const resA = await apollo.execute(UpdateProjectRegionDocument, {
projectId: testProject.id,
regionKey: regionKey2
})
expect(resA).to.not.haveGraphQLErrors()
await ensureProjectRegion(testProject.id, regionKey2)
const resB = await apollo.execute(GetRegionalProjectBlobDocument, {
projectId: testProject.id,
blobId: testBlobId
})
expect(resB).to.not.haveGraphQLErrors()
expect(resB.data?.project.blob).to.not.be.undefined
})
})
: void 0
@@ -93,7 +93,7 @@ describe('Workspace GQL Subscriptions', () => {
]
modes.forEach(({ isMultiRegion }) => {
describe(`W/${!isMultiRegion ? 'o' : ''} multiregion`, () => {
describe(`W/${!isMultiRegion ? 'o' : ''} @multiregion`, () => {
const myMainWorkspace: BasicTestWorkspace = {
id: '',
ownerId: '',
@@ -27,6 +27,20 @@
"endpoint": "http://127.0.0.1:9020",
"s3Region": "us-east-1"
}
},
"region2": {
"postgres": {
"connectionUri": "postgresql://speckle:speckle@127.0.0.1:5402/speckle2_test",
"privateConnectionUri": "postgresql://speckle:speckle@postgres-region2:5432/speckle2_test"
},
"blobStorage": {
"accessKey": "minioadmin",
"secretKey": "minioadmin",
"bucket": "test-speckle-server",
"createBucketIfNotExists": true,
"endpoint": "http://127.0.0.1:9040",
"s3Region": "us-east-1"
}
}
}
}
+1 -1
View File
@@ -25,7 +25,7 @@
"ts-mocha": "node --require ts-node/register ./bin/mocha",
"test": "cross-env NODE_ENV=test LOG_LEVEL=silent LOG_PRETTY=true yarn ts-mocha",
"test:all-ff": "cross-env ENABLE_ALL_FFS=true yarn test",
"test:multiregion": "cross-env RUN_TESTS_IN_MULTIREGION_MODE=true FF_WORKSPACES_MODULE_ENABLED=true FF_WORKSPACES_MULTI_REGION_ENABLED=true yarn test",
"test:multiregion": "cross-env RUN_TESTS_IN_MULTIREGION_MODE=true FF_WORKSPACES_MODULE_ENABLED=true FF_WORKSPACES_MULTI_REGION_ENABLED=true yarn test --grep @multiregion",
"test:no-ff": "cross-env DISABLE_ALL_FFS=true yarn test",
"test:coverage": "cross-env NODE_ENV=test LOG_LEVEL=silent LOG_PRETTY=true nyc --reporter lcov yarn ts-mocha",
"test:report": "MOCHA_FILE=reports/test-results.xml yarn test:coverage -- --reporter mocha-multi --reporter-options spec=-,mocha-junit-reporter=reports/test-results.xml",
+3 -1
View File
@@ -196,15 +196,17 @@ export const resetPubSubFactory = (deps: { db: Knex }) => async () => {
rows: Array<{ pubname: string }>
}
// If we do not wait, the following call occasionally fails because a replication slot is still in use.
const dropSubs = async (info: SubInfo) => {
await wait(1000)
await deps.db.raw(
`SELECT * FROM aiven_extras.pg_alter_subscription_disable('${info.subname}');`
)
// If we do not wait, the following call occasionally fails because a replication slot is still in use.
await wait(1000)
await deps.db.raw(
`SELECT * FROM aiven_extras.pg_drop_subscription('${info.subname}');`
)
await wait(1000)
await deps.db.raw(
`SELECT * FROM aiven_extras.dblink_slot_create_or_drop('${info.subconninfo}', '${info.subslotname}', 'drop');`
)
-6
View File
@@ -64,11 +64,6 @@ export const parseFeatureFlags = (
schema: z.boolean(),
defaults: { production: false, _: false }
},
// Toggles IFC parsing with experimental .Net parser
FF_FILEIMPORT_IFC_DOTNET_ENABLED: {
schema: z.boolean(),
defaults: { production: false, _: false }
},
// Forces onboarding for all users
FF_FORCE_ONBOARDING: {
schema: z.boolean(),
@@ -114,7 +109,6 @@ export type FeatureFlags = {
FF_GATEKEEPER_FORCE_FREE_PLAN: boolean
FF_BILLING_INTEGRATION_ENABLED: boolean
FF_WORKSPACES_MULTI_REGION_ENABLED: boolean
FF_FILEIMPORT_IFC_DOTNET_ENABLED: boolean
FF_FORCE_ONBOARDING: boolean
FF_OBJECTS_STREAMING_FIX: boolean
FF_MOVE_PROJECT_REGION_ENABLED: boolean
+17
View File
@@ -15,6 +15,23 @@
class="relative overflow-y-scroll h-full pointer-events-none w-auto"
></div>
</div>
<div class="center-wrapper" id="loadingWrapper">
<div class="loading-container">
<!-- Grayscale Image -->
<img
class="grayscale-overlay"
src="https://avatars.githubusercontent.com/u/65039012?s=280&v=4"
alt="Grayscale Image"
/>
<!-- Colored Image -->
<img
class="color-image"
id="colorImage"
src="https://avatars.githubusercontent.com/u/65039012?s=280&v=4"
alt="Color Image"
/>
</div>
</div>
<script type="module" src="/src/main.ts"></script>
<!-- <div class="w-screen h-screen flex flex-col" id="multi-root">
<div class="h-1/2">
+5 -3
View File
@@ -1270,6 +1270,7 @@ export default class Sandbox {
}
public async loadUrl(url: string) {
const colorImage = document.getElementById('colorImage')
const authToken = localStorage.getItem(
url.includes('latest') ? 'AuthTokenLatest' : 'AuthToken'
) as string
@@ -1284,9 +1285,10 @@ export default class Sandbox {
undefined
)
/** Too spammy */
// loader.on(LoaderEvent.LoadProgress, (arg: { progress: number; id: string }) => {
// console.warn(arg)
// })
loader.on(LoaderEvent.LoadProgress, (arg: { progress: number; id: string }) => {
if (colorImage)
colorImage.style.clipPath = `inset(${(1 - arg.progress) * 100}% 0 0 0)`
})
loader.on(LoaderEvent.LoadCancelled, (resource: string) => {
console.warn(`Resource ${resource} loading was canceled`)
})
+19 -9
View File
@@ -74,6 +74,14 @@ const createViewer = async (containerName: string, _stream: string) => {
Object.assign(sandbox.sceneParams.worldSize, viewer.World.worldSize)
Object.assign(sandbox.sceneParams.worldOrigin, viewer.World.worldOrigin)
sandbox.refresh()
const loadingWrapper = document.getElementById('loadingWrapper')
if (loadingWrapper) {
loadingWrapper.addEventListener('transitionend', function () {
// Remove the loading wrapper from the page
loadingWrapper.style.display = 'none'
})
loadingWrapper.style.opacity = '0'
}
})
viewer.on(ViewerEvent.UnloadComplete, () => {
@@ -94,22 +102,16 @@ const createViewer = async (containerName: string, _stream: string) => {
sandbox.makeDiffUI()
sandbox.makeMeasurementsUI()
await sandbox.objectLoaderOnly(_stream)
// await sandbox.loadUrl(_stream)
// await sandbox.objectLoaderOnly(_stream)
await sandbox.loadUrl(_stream)
// await sandbox.loadJSON(JSONSpeckleStream)
}
const getStream = () => {
return (
// prettier-ignore
`https://latest.speckle.systems/projects/97750296c2/models/767b70fc63@5386a0af02`
//crashing out of memory?
//`https://latest.speckle.systems/projects/97750296c2/models/767b70fc63@2a6fd781f2`
//too big?
// `https://latest.speckle.systems/projects/126cd4b7bb/models/032d09f716`
// 'https://app.speckle.systems/streams/da9e320dad/commits/5388ef24b8?c=%5B-7.66134,10.82932,6.41935,-0.07739,-13.88552,1.8697,0,1%5D'
// Revit sample house (good for bim-like stuff with many display meshes)
//'https://app.speckle.systems/streams/da9e320dad/commits/5388ef24b8'
'https://app.speckle.systems/streams/da9e320dad/commits/5388ef24b8'
// 'https://latest.speckle.systems/streams/c1faab5c62/commits/ab1a1ab2b6'
// 'https://app.speckle.systems/streams/da9e320dad/commits/5388ef24b8'
// 'https://latest.speckle.systems/streams/58b5648c4d/commits/60371ecb2d'
@@ -502,6 +504,14 @@ const getStream = () => {
// SUPER slow tree build time (LARGE N-GONS TRIANGULATION)
// 'https://app.speckle.systems/projects/0edb6ef628/models/ff3d8480bc@cd83d90a2c'
/* ObjectLoader 2 tests */
// `https://latest.speckle.systems/projects/97750296c2/models/767b70fc63@5386a0af02`
//crashing out of memory?
//`https://latest.speckle.systems/projects/97750296c2/models/767b70fc63@2a6fd781f2`
//too big?
// `https://latest.speckle.systems/projects/126cd4b7bb/models/032d09f716`
// 'https://app.speckle.systems/streams/da9e320dad/commits/5388ef24b8?c=%5B-7.66134,10.82932,6.41935,-0.07739,-13.88552,1.8697,0,1%5D'
)
}
+38
View File
@@ -60,3 +60,41 @@ canvas {
border-radius: 0.1rem;
border: 4px solid rgb(129, 129, 129);
}
.center-wrapper {
position: absolute;
top: 95%;
left: 92%;
transform: translate(-50%, -50%) scale(0.5);
transition: opacity 0.5s ease; /* Smooth fade-out for the whole widget */
}
.loading-container {
position: relative;
width: 280px;
height: 280px;
overflow: hidden;
}
/* Grayscale version */
.grayscale-overlay,
.color-image {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
transition: opacity 0.8s ease-in-out; /* Smooth fade-out */
}
.grayscale-overlay {
filter: grayscale(100%);
z-index: 1;
opacity: 1; /* Fully visible by default */
}
/* Color version */
.color-image {
z-index: 2;
clip-path: inset(100% 0 0 0);
}
+3 -1
View File
@@ -23,7 +23,8 @@ import {
type SpeckleView,
type SunLightConfiguration,
type ViewerParams,
StencilOutlineType
StencilOutlineType,
UpdateFlags
} from '../IViewer.js'
import { Viewer } from './Viewer.js'
import { SectionTool } from './extensions/SectionTool.js'
@@ -207,6 +208,7 @@ export class LegacyViewer extends Viewer {
this.selection.clearSelection()
if (this.filtering.filteringState.selectedObjects)
this.filtering.filteringState.selectedObjects.length = 0
this.requestRender(UpdateFlags.RENDER | UpdateFlags.SHADOWS)
return Promise.resolve(this.filtering.filteringState)
}
@@ -101,15 +101,16 @@ export class Geometry {
for (let i = 0; i < indexAttributes.length; ++i) {
const index = indexAttributes[i]
if (!index || !positionAttributes) {
const positions = positionAttributes[i]
if (!index || !positions) {
throw new Error('Cannot merge geometries. Indices or positions are undefined')
}
for (let j = 0; j < index.length; ++j) {
mergedIndex.push(index[j] + indexOffset)
mergedIndex.push(index[j] + indexOffset / 3)
}
indexOffset += positionAttributes.length
indexOffset += positions.length
}
return mergedIndex
}
@@ -115,10 +115,6 @@ spec:
- name: MULTI_REGION_CONFIG_PATH
value: "/multi-region-config/multi-region-config.json"
{{- end }}
{{- if .Values.featureFlags.fileImportIFCDotNetEnabled }}
- name: FF_FILEIMPORT_IFC_DOTNET_ENABLED
value: {{ .Values.featureFlags.fileImportIFCDotNetEnabled | quote }}
{{- end }}
{{- if .Values.fileimport_service.affinity }}
affinity: {{- include "speckle.renderTpl" (dict "value" .Values.fileimport_service.affinity "context" $) | nindent 8 }}
{{- end }}
@@ -75,11 +75,6 @@
"description": "Toggles whether multi-region is available within workspaces. workspacesModuleEnabled must also be enabled.",
"default": false
},
"fileImportIFCDotNetEnabled": {
"type": "boolean",
"description": "Toggles whether the experimental .Net IFC importer is used for importing IFC files.",
"default": false
},
"forceEmailVerification": {
"type": "boolean",
"description": "Forces email verification for all users",
-2
View File
@@ -51,8 +51,6 @@ featureFlags:
billingIntegrationEnabled: false
## @param featureFlags.workspacesMultiRegionEnabled Toggles whether multi-region is available within workspaces. workspacesModuleEnabled must also be enabled.
workspacesMultiRegionEnabled: false
## @param featureFlags.fileImportIFCDotNetEnabled Toggles whether the experimental .Net IFC importer is used for importing IFC files.
fileImportIFCDotNetEnabled: false
## @param featureFlags.forceEmailVerification Forces email verification for all users
forceEmailVerification: false
## @param featureFlags.forceOnboarding Forces onboarding for all users