diff --git a/.circleci/config.yml b/.circleci/config.yml index b672554095..3a27e23dd1 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -595,13 +595,13 @@ jobs: POSTGRES_DB: speckle2_test POSTGRES_PASSWORD: speckle POSTGRES_USER: speckle - command: -c 'max_connections=1000' + command: -c 'max_connections=1000' -c 'wal_level=logical' - image: 'speckle/speckle-postgres' environment: POSTGRES_DB: speckle2_test POSTGRES_PASSWORD: speckle POSTGRES_USER: speckle - command: -c 'max_connections=1000' -c 'port=5433' + command: -c 'max_connections=1000' -c 'port=5433' -c 'wal_level=logical' - image: 'minio/minio' command: server /data --console-address ":9001" environment: @@ -623,8 +623,10 @@ jobs: S3_REGION: '' # optional, defaults to 'us-east-1' AUTOMATE_ENCRYPTION_KEYS_PATH: 'test/assets/automate/encryptionKeys.json' FF_BILLING_INTEGRATION_ENABLED: 'true' - # These are the only 2 different env keys: + # These are the only different env keys: MULTI_REGION_CONFIG_PATH: '../../.circleci/multiregion.test-ci.json' + FF_WORKSPACES_MODULE_ENABLED: 'true' + FF_WORKSPACES_MULTI_REGION_ENABLED: 'true' RUN_TESTS_IN_MULTIREGION_MODE: true test-frontend-2: diff --git a/packages/frontend-2/package.json b/packages/frontend-2/package.json index d9b1f91d09..ac7bbdf28b 100644 --- a/packages/frontend-2/package.json +++ b/packages/frontend-2/package.json @@ -89,11 +89,11 @@ "@babel/preset-typescript": "^7.18.6", "@datadog/datadog-ci": "^2.37.0", "@eslint/config-inspector": "^0.4.10", - "@graphql-codegen/cli": "^5.0.2", - "@graphql-codegen/client-preset": "^4.3.0", - "@graphql-codegen/plugin-helpers": "^5.0.4", - "@graphql-codegen/typescript": "^4.0.9", - "@graphql-codegen/visitor-plugin-common": "5.3.1", + "@graphql-codegen/cli": "^5.0.3", + "@graphql-codegen/client-preset": "^4.5.0", + "@graphql-codegen/plugin-helpers": "^5.1.0", + "@graphql-codegen/typescript": "^4.1.1", + "@graphql-codegen/visitor-plugin-common": "5.5.0", "@nuxt/devtools": "^1.3.9", "@nuxt/eslint": "^0.3.13", "@nuxt/image": "^1.8.1", diff --git a/packages/server/db/knex.ts b/packages/server/db/knex.ts index d8a03f2cc6..f5bcc0cf10 100644 --- a/packages/server/db/knex.ts +++ b/packages/server/db/knex.ts @@ -24,6 +24,7 @@ export default knexInstance export { knexInstance as db, knexInstance as knex, - knexInstance as knexInstance, + knexInstance as mainDb, + knexInstance, config } diff --git a/packages/server/modules/cli/commands/workspaces.ts b/packages/server/modules/cli/commands/workspaces.ts new file mode 100644 index 0000000000..9213712e42 --- /dev/null +++ b/packages/server/modules/cli/commands/workspaces.ts @@ -0,0 +1,13 @@ +import { noop } from 'lodash' +import { CommandModule } from 'yargs' + +const command: CommandModule = { + command: 'workspaces', + describe: 'Various workspace related actions', + builder(yargs) { + return yargs.commandDir('workspaces', { extensions: ['js', 'ts'] }).demandCommand() + }, + handler: noop +} + +export = command diff --git a/packages/server/modules/cli/commands/workspaces/set-plan.ts b/packages/server/modules/cli/commands/workspaces/set-plan.ts new file mode 100644 index 0000000000..292d66d41d --- /dev/null +++ b/packages/server/modules/cli/commands/workspaces/set-plan.ts @@ -0,0 +1,67 @@ +import { CommandModule } from 'yargs' +import { cliLogger } from '@/logging/logging' +import { getWorkspaceBySlugOrIdFactory } from '@/modules/workspaces/repositories/workspaces' +import { db } from '@/db/knex' +import { + PaidWorkspacePlanStatuses, + PlanStatuses +} from '@/modules/gatekeeper/domain/billing' +import { upsertPaidWorkspacePlanFactory } from '@/modules/gatekeeper/repositories/billing' +import { PaidWorkspacePlans } from '@/modules/gatekeeper/domain/workspacePricing' + +const command: CommandModule< + unknown, + { + workspaceSlugOrId: string + status: PlanStatuses + plan: PaidWorkspacePlans + } +> = { + command: 'set-plan [plan] [status]', + describe: 'Set a plan for a workspace.', + builder: { + workspaceSlugOrId: { + describe: 'Workspace ID or slug', + type: 'string' + }, + plan: { + describe: 'Plan to set the status for', + type: 'string', + default: 'business', + choices: ['business', 'team', 'pro'] + }, + status: { + describe: 'Status to set for the workspace plan', + type: 'string', + default: 'valid', + choices: [ + 'valid', + 'trial', + 'expired', + 'paymentFailed', + 'cancelationScheduled', + 'canceled' + ] + } + }, + handler: async (args) => { + cliLogger.info( + `Setting plan for workspace '${args.workspaceSlugOrId}' to '${args.plan}' with status '${args.status}'` + ) + const workspace = await getWorkspaceBySlugOrIdFactory({ db })(args) + if (!workspace) { + throw new Error(`Workspace w/ slug or id '${args.workspaceSlugOrId}' not found`) + } + + await upsertPaidWorkspacePlanFactory({ db })({ + workspacePlan: { + workspaceId: workspace.id, + name: args.plan, + status: args.status as PaidWorkspacePlanStatuses + } + }) + cliLogger.info(`Plan set!`) + } +} + +export = command diff --git a/packages/server/modules/comments/repositories/comments.ts b/packages/server/modules/comments/repositories/comments.ts index 331358baf5..887a2d26f7 100644 --- a/packages/server/modules/comments/repositories/comments.ts +++ b/packages/server/modules/comments/repositories/comments.ts @@ -411,7 +411,7 @@ export const getPaginatedCommitCommentsTotalCountFactory = (deps: { db: Knex }): GetPaginatedCommitCommentsTotalCount => async (params: Omit) => { const baseQ = getPaginatedCommitCommentsBaseQueryFactory(deps)(params) - const q = knex.count<{ count: string }[]>().from(baseQ.as('sq1')) + const q = deps.db.count<{ count: string }[]>().from(baseQ.as('sq1')) const [row] = await q return parseInt(row.count || '0') @@ -476,7 +476,7 @@ export const getPaginatedBranchCommentsTotalCountFactory = (deps: { db: Knex }) => async (params: Omit) => { const baseQ = getPaginatedBranchCommentsBaseQueryFactory(deps)(params) - const q = knex.count<{ count: string }[]>().from(baseQ.as('sq1')) + const q = deps.db.count<{ count: string }[]>().from(baseQ.as('sq1')) const [row] = await q return parseInt(row.count || '0') @@ -673,7 +673,7 @@ export const getPaginatedProjectCommentsTotalCountFactory = params, options ) - const q = knex.count<{ count: string }[]>().from(baseQuery.as('sq1')) + const q = deps.db.count<{ count: string }[]>().from(baseQuery.as('sq1')) const [row] = await q return parseInt(row.count || '0') @@ -866,7 +866,12 @@ export const getCommentsLegacyFactory = query.orderBy('createdAt', 'desc') query.limit(limit || 1) // need at least 1 row to get totalCount - const rows = await query + const rows = (await query) as Array< + CommentRecord & { + total_count: string + resources: Array<{ resourceId: string; resourceType: string }> + } + > const totalCount = rows && rows.length > 0 ? parseInt(rows[0].total_count) : 0 const nextCursor = rows && rows.length > 0 ? rows[rows.length - 1].createdAt : null diff --git a/packages/server/modules/core/graph/resolvers/versions.ts b/packages/server/modules/core/graph/resolvers/versions.ts index 5fce4d17d1..1f89adf0d6 100644 --- a/packages/server/modules/core/graph/resolvers/versions.ts +++ b/packages/server/modules/core/graph/resolvers/versions.ts @@ -103,7 +103,6 @@ export = { }, VersionMutations: { async moveToModel(_parent, args, ctx) { - // TODO: how to get streamId here? const projectId = args.input.projectId const projectDb = await getProjectDbClient({ projectId }) @@ -121,7 +120,6 @@ export = { return await batchMoveCommits(args.input, ctx.userId!) }, async delete(_parent, args, ctx) { - // TODO: how to get streamId here? const projectId = args.input.projectId const projectDb = await getProjectDbClient({ projectId }) @@ -138,7 +136,6 @@ export = { return true }, async update(_parent, args, ctx) { - // TODO: how to get streamId here? const projectId = args.input.projectId const projectDb = await getProjectDbClient({ projectId }) const stream = await ctx.loaders diff --git a/packages/server/modules/core/helpers/graphTypes.ts b/packages/server/modules/core/helpers/graphTypes.ts index e0dd6551b8..f49a8f5857 100644 --- a/packages/server/modules/core/helpers/graphTypes.ts +++ b/packages/server/modules/core/helpers/graphTypes.ts @@ -1,5 +1,6 @@ import { CommitWithStreamBranchId, + CommitWithStreamId, LegacyStreamCommit, LegacyUserCommit } from '@/modules/core/domain/commits/types' @@ -12,7 +13,6 @@ import { import { Roles, ServerRoles, StreamRoles } from '@/modules/core/helpers/mainConstants' import { BranchRecord, - CommitRecord, ObjectRecord, ServerInfo, StreamRecord, @@ -36,7 +36,10 @@ export type StreamGraphQLReturn = StreamRecord & { role?: string | null } -export type CommitGraphQLReturn = CommitRecord | LegacyStreamCommit | LegacyUserCommit +export type CommitGraphQLReturn = + | CommitWithStreamId + | LegacyStreamCommit + | LegacyUserCommit export type BranchGraphQLReturn = BranchRecord diff --git a/packages/server/modules/core/loaders.ts b/packages/server/modules/core/loaders.ts index 4d3f729373..e0dbe36b78 100644 --- a/packages/server/modules/core/loaders.ts +++ b/packages/server/modules/core/loaders.ts @@ -94,6 +94,10 @@ export async function buildRequestLoaders( * Get dataloaders for specific region */ const forRegion = (deps: { db: Knex }) => { + if (deps.db === mainDb) { + return mainDbLoaders + } + if (!regionLoaders.has(deps.db)) { regionLoaders.set(deps.db, createLoadersForRegion(deps)) } diff --git a/packages/server/modules/core/repositories/branches.ts b/packages/server/modules/core/repositories/branches.ts index 52aa58a4b5..ea09c2ce34 100644 --- a/packages/server/modules/core/repositories/branches.ts +++ b/packages/server/modules/core/repositories/branches.ts @@ -404,7 +404,7 @@ export const getPaginatedProjectModelsTotalCountFactory = } const baseQ = getPaginatedProjectModelsBaseQueryFactory(deps)(projectId, params) - const q = knex.count<{ count: string }[]>().from(baseQ.as('sq1')) + const q = deps.db.count<{ count: string }[]>().from(baseQ.as('sq1')) const [res] = await q return parseInt(res?.count || '0') diff --git a/packages/server/modules/fileuploads/tests/fileuploads.integration.spec.ts b/packages/server/modules/fileuploads/tests/fileuploads.integration.spec.ts index 2256439d0a..1f72eaf0b2 100644 --- a/packages/server/modules/fileuploads/tests/fileuploads.integration.spec.ts +++ b/packages/server/modules/fileuploads/tests/fileuploads.integration.spec.ts @@ -343,7 +343,7 @@ describe('FileUploads @fileuploads', () => { .set('Authorization', `Bearer ${userOneToken}`) .set('Accept', 'application/json') .attach('test.ifc', require.resolve('@/readme.md'), 'test.ifc') - expect(response.statusCode).to.equal(500) //FIXME should be 404 (technically a 401, but we don't want to leak existence of stream so 404 is preferrable) + expect(response.statusCode).to.equal(404) //FIXME should be 404 (technically a 401, but we don't want to leak existence of stream so 404 is preferrable) const gqlResponse = await sendRequest(userOneToken, { query: `query ($streamId: String!) { stream(id: $streamId) { diff --git a/packages/server/modules/gatekeeper/domain/billing.ts b/packages/server/modules/gatekeeper/domain/billing.ts index 3414c9f6d3..27ec5be834 100644 --- a/packages/server/modules/gatekeeper/domain/billing.ts +++ b/packages/server/modules/gatekeeper/domain/billing.ts @@ -19,6 +19,11 @@ export type PaidWorkspacePlanStatuses = export type TrialWorkspacePlanStatuses = 'trial' | 'expired' +export type PlanStatuses = + | PaidWorkspacePlanStatuses + | TrialWorkspacePlanStatuses + | UnpaidWorkspacePlanStatuses + type BaseWorkspacePlan = { workspaceId: string } diff --git a/packages/server/modules/multiregion/regionConfig.ts b/packages/server/modules/multiregion/regionConfig.ts index db13cce516..ff28f8b846 100644 --- a/packages/server/modules/multiregion/regionConfig.ts +++ b/packages/server/modules/multiregion/regionConfig.ts @@ -17,13 +17,15 @@ import { let multiRegionConfig: Optional = undefined const getMultiRegionConfig = async (): Promise => { + const emptyReturn = () => ({ main: { postgres: { connectionUri: '' } }, regions: {} }) + if (isDevOrTestEnv() && !isMultiRegionEnabled()) { - // this should throw somehow - return { main: { postgres: { connectionUri: '' } }, regions: {} } + return emptyReturn() } if (!multiRegionConfig) { - const relativePath = getMultiRegionConfigPath() + const relativePath = getMultiRegionConfigPath({ unsafe: isDevOrTestEnv() }) + if (!relativePath) return emptyReturn() const configPath = path.resolve(packageRoot, relativePath) diff --git a/packages/server/modules/multiregion/tests/e2e/serverAdmin.graph.spec.ts b/packages/server/modules/multiregion/tests/e2e/serverAdmin.graph.spec.ts index 535b587b2c..61389c439f 100644 --- a/packages/server/modules/multiregion/tests/e2e/serverAdmin.graph.spec.ts +++ b/packages/server/modules/multiregion/tests/e2e/serverAdmin.graph.spec.ts @@ -1,5 +1,5 @@ import { DataRegionsConfig } from '@/modules/multiregion/domain/types' -import { Regions } from '@/modules/multiregion/repositories' +import { isMultiRegionEnabled } from '@/modules/multiregion/helpers' import { BasicTestUser, createTestUser } from '@/test/authHelper' import { CreateNewRegionDocument, @@ -14,260 +14,269 @@ import { testApolloServer, TestApolloServer } from '@/test/graphqlHelper' -import { beforeEachContext, truncateTables } from '@/test/hooks' +import { beforeEachContext, getRegionKeys } from '@/test/hooks' import { MultiRegionConfigMock, MultiRegionDbSelectorMock } from '@/test/mocks/global' +import { truncateRegionsSafely } from '@/test/speckle-helpers/regions' import { Roles } from '@speckle/shared' import { expect } from 'chai' -describe('Multi Region Server Settings', () => { - let testAdminUser: BasicTestUser - let testBasicUser: BasicTestUser - let apollo: TestApolloServer +const isEnabled = isMultiRegionEnabled() - const fakeRegionKey1 = 'us-west-1' - const fakeRegionKey2 = 'eu-east-2' +isEnabled + ? describe('Multi Region Server Settings', () => { + let testAdminUser: BasicTestUser + let testBasicUser: BasicTestUser + let apollo: TestApolloServer - const fakeRegionConfig: DataRegionsConfig = { - [fakeRegionKey1]: { - postgres: { - connectionUri: 'postgres://user:password@uswest1:port/dbname' - } - }, - [fakeRegionKey2]: { - postgres: { - connectionUri: 'postgres://user:password@eueast3:port/dbname' - } - } - } - - before(async () => { - MultiRegionConfigMock.mockFunction( - 'getAvailableRegionConfig', - async () => fakeRegionConfig - ) - MultiRegionDbSelectorMock.mockFunction('initializeRegion', async () => - Promise.resolve() - ) - - await beforeEachContext() - testAdminUser = await createTestUser({ role: Roles.Server.Admin }) - testBasicUser = await createTestUser({ role: Roles.Server.User }) - apollo = await testApolloServer({ authUserId: testAdminUser.id }) - }) - - after(() => { - MultiRegionConfigMock.resetMockedFunctions() - MultiRegionDbSelectorMock.resetMockedFunctions() - }) - - describe('server config', () => { - const createRegion = ( - input: CreateServerRegionInput, - options?: ExecuteOperationOptions - ) => apollo.execute(CreateNewRegionDocument, { input }, options) - - it("region keys can't be retrieved by non-admin", async () => { - const res = await apollo.execute( - GetAvailableRegionKeysDocument, - {}, - { - context: { - userId: testBasicUser.id, - role: Roles.Server.User - } - } - ) - expect(res).to.haveGraphQLErrors('You do not have the required server role') - expect(res.data?.serverInfo.multiRegion.availableKeys).to.be.not.ok - }) - - it('allows retrieving available config keys', async () => { - const res = await apollo.execute(GetAvailableRegionKeysDocument, {}) - expect(res.data?.serverInfo.multiRegion.availableKeys).to.deep.equal( - Object.keys(fakeRegionConfig) - ) - expect(res).to.not.haveGraphQLErrors() - }) - - describe('when creating new region', async () => { - afterEach(async () => { - // Wipe created regions - await truncateTables([Regions.name]) - }) + const fakeRegionKey1 = 'us-west-1' + const fakeRegionKey2 = 'eu-east-2' - it("it can't be created by non-admin", async () => { - const res = await createRegion( - { - key: fakeRegionKey1, - name: 'US West 1', - description: 'Helloooo' - }, - { - context: { - userId: testBasicUser.id, - role: Roles.Server.User - } + const fakeRegionConfig: DataRegionsConfig = { + [fakeRegionKey1]: { + postgres: { + connectionUri: 'postgres://user:password@uswest1:port/dbname' + } + }, + [fakeRegionKey2]: { + postgres: { + connectionUri: 'postgres://user:password@eueast3:port/dbname' } - ) - expect(res).to.haveGraphQLErrors('You do not have the required server role') - }) - - it('it works with valid input', async () => { - const input: CreateServerRegionInput = { - key: fakeRegionKey1, - name: 'US West 1', - description: 'Helloooo' - } - - const res = await createRegion(input) - expect(res).to.not.haveGraphQLErrors() - expect(res.data?.serverInfoMutations.multiRegion.create).to.deep.equal({ - ...input, - id: input.key - }) - }) - - it("doesn't work with already used up key", async () => { - const input: CreateServerRegionInput = { - key: fakeRegionKey1, - name: 'US West 1', - description: 'Helloooo' - } - - const res1 = await createRegion(input) - expect(res1).to.not.haveGraphQLErrors() - - const res2 = await createRegion(input) - expect(res2).to.haveGraphQLErrors('Region with this key already exists') - }) - - it("doesn't work with invalid key", async () => { - const input: CreateServerRegionInput = { - key: 'randooo-key', - name: 'US West 1', - description: 'Helloooo' } - - const res = await createRegion(input) - expect(res).to.haveGraphQLErrors('Region key is not valid') - }) - }) - - describe('when working with existing regions', async () => { - const createdRegionInput: CreateServerRegionInput = { - key: fakeRegionKey1, - name: 'US West 1', - description: 'Helloooo' } before(async () => { - // Create a region - await createRegion(createdRegionInput, { assertNoErrors: true }) - }) - - it("can't retrieve regions if non-admin", async () => { - const res = await apollo.execute( - GetRegionsDocument, - {}, - { - context: { - userId: testBasicUser.id, - role: Roles.Server.User - } - } + MultiRegionConfigMock.mockFunction( + 'getAvailableRegionConfig', + async () => fakeRegionConfig + ) + MultiRegionDbSelectorMock.mockFunction('initializeRegion', async () => + Promise.resolve() ) - expect(res).to.haveGraphQLErrors('You do not have the required server role') - }) - it('allows retrieving all regions', async () => { - const res = await apollo.execute(GetRegionsDocument, {}) + await beforeEachContext() + testAdminUser = await createTestUser({ role: Roles.Server.Admin }) + testBasicUser = await createTestUser({ role: Roles.Server.User }) + apollo = await testApolloServer({ authUserId: testAdminUser.id }) + }) - expect(res).to.not.haveGraphQLErrors() - expect(res.data?.serverInfo.multiRegion.regions).to.have.length(1) - expect(res.data?.serverInfo.multiRegion.regions).to.deep.equal([ - { - ...createdRegionInput, - id: createdRegionInput.key - } - ]) + after(() => { + MultiRegionConfigMock.resetMockedFunctions() + MultiRegionDbSelectorMock.resetMockedFunctions() }) - it('filters out used region from available keys', async () => { - const res = await apollo.execute(GetAvailableRegionKeysDocument, {}) + describe('server config', () => { + const createRegion = ( + input: CreateServerRegionInput, + options?: ExecuteOperationOptions + ) => apollo.execute(CreateNewRegionDocument, { input }, options) + + it("region keys can't be retrieved by non-admin", async () => { + const res = await apollo.execute( + GetAvailableRegionKeysDocument, + {}, + { + context: { + userId: testBasicUser.id, + role: Roles.Server.User + } + } + ) + expect(res).to.haveGraphQLErrors('You do not have the required server role') + expect(res.data?.serverInfo.multiRegion.availableKeys).to.be.not.ok + }) - expect(res).to.not.haveGraphQLErrors() - expect(res.data?.serverInfo.multiRegion.availableKeys).to.deep.equal([ - fakeRegionKey2 - ]) - }) - }) + it('allows retrieving available config keys', async () => { + const res = await apollo.execute(GetAvailableRegionKeysDocument, {}) + expect(res.data?.serverInfo.multiRegion.availableKeys).to.deep.equal( + Object.keys(fakeRegionConfig) + ) + expect(res).to.not.haveGraphQLErrors() + }) - describe('when updating existing region', async () => { - const createdRegionInput: CreateServerRegionInput = { - key: fakeRegionKey2, - name: 'Updatable Region 1', - description: 'Helloooo' - } + describe('when creating new region', async () => { + afterEach(async () => { + // Wipe created regions + await truncateRegionsSafely() + }) + + it("it can't be created by non-admin", async () => { + const res = await createRegion( + { + key: fakeRegionKey1, + name: 'US West 1', + description: 'Helloooo' + }, + { + context: { + userId: testBasicUser.id, + role: Roles.Server.User + } + } + ) + expect(res).to.haveGraphQLErrors('You do not have the required server role') + }) + + it('it works with valid input', async () => { + const input: CreateServerRegionInput = { + key: fakeRegionKey1, + name: 'US West 1', + description: 'Helloooo' + } - const updateRegion = ( - input: UpdateServerRegionInput, - options?: ExecuteOperationOptions - ) => apollo.execute(UpdateRegionDocument, { input }, options) + const res = await createRegion(input) + expect(res).to.not.haveGraphQLErrors() + expect(res.data?.serverInfoMutations.multiRegion.create).to.deep.equal({ + ...input, + id: input.key + }) + }) + + it("doesn't work with already used up key", async () => { + const input: CreateServerRegionInput = { + key: fakeRegionKey1, + name: 'US West 1', + description: 'Helloooo' + } - beforeEach(async () => { - // Create new region - await createRegion(createdRegionInput, { assertNoErrors: true }) - }) + const res1 = await createRegion(input) + expect(res1).to.not.haveGraphQLErrors() - afterEach(async () => { - // Wipe created regions - await truncateTables([Regions.name]) - }) + const res2 = await createRegion(input) + expect(res2).to.haveGraphQLErrors('Region with this key already exists') + }) - it("can't update region if non-admin", async () => { - const res = await updateRegion( - { - key: createdRegionInput.key, - name: 'Updated Region 1' - }, - { - context: { - userId: testBasicUser.id, - role: Roles.Server.User + it("doesn't work with invalid key", async () => { + const input: CreateServerRegionInput = { + key: 'randooo-key', + name: 'US West 1', + description: 'Helloooo' } - } - ) - expect(res).to.haveGraphQLErrors('You do not have the required server role') - }) - it('works with valid input', async () => { - const updatedName = 'aaa Updated Region 1' - const updatedDescription = 'bbb Updated description' - - const res = await updateRegion({ - key: createdRegionInput.key, - name: updatedName, - description: updatedDescription + const res = await createRegion(input) + expect(res).to.haveGraphQLErrors('Region key is not valid') + }) }) - expect(res.data?.serverInfoMutations.multiRegion.update).to.deep.equal({ - ...createdRegionInput, - id: createdRegionInput.key, - name: updatedName, - description: updatedDescription - }) - expect(res).to.not.haveGraphQLErrors() - }) + describe('when working with existing regions', async () => { + const createdRegionInput: CreateServerRegionInput = { + key: fakeRegionKey1, + name: 'US West 1', + description: 'Helloooo' + } - it('fails gracefully with invalid region key', async () => { - const res = await updateRegion({ - key: 'invalid-key', - name: 'Updated Region 1' + before(async () => { + // Create a region + await createRegion(createdRegionInput, { assertNoErrors: true }) + }) + + it("can't retrieve regions if non-admin", async () => { + const res = await apollo.execute( + GetRegionsDocument, + {}, + { + context: { + userId: testBasicUser.id, + role: Roles.Server.User + } + } + ) + expect(res).to.haveGraphQLErrors('You do not have the required server role') + }) + + it('allows retrieving all regions', async () => { + const res = await apollo.execute(GetRegionsDocument, {}) + + expect(res).to.not.haveGraphQLErrors() + expect(res.data?.serverInfo.multiRegion.regions).to.have.length( + 1 + getRegionKeys().length + ) + expect( + res.data?.serverInfo.multiRegion.regions.find( + (r) => r.id === createdRegionInput.key + ) + ).to.deep.equal({ + ...createdRegionInput, + id: createdRegionInput.key + }) + }) + + it('filters out used region from available keys', async () => { + const res = await apollo.execute(GetAvailableRegionKeysDocument, {}) + + expect(res).to.not.haveGraphQLErrors() + expect(res.data?.serverInfo.multiRegion.availableKeys).to.deep.equal([ + fakeRegionKey2 + ]) + }) }) - expect(res).to.haveGraphQLErrors('Region not found') - expect(res.data?.serverInfoMutations.multiRegion.update).to.be.not.ok + describe('when updating existing region', async () => { + const createdRegionInput: CreateServerRegionInput = { + key: fakeRegionKey2, + name: 'Updatable Region 1', + description: 'Helloooo' + } + + const updateRegion = ( + input: UpdateServerRegionInput, + options?: ExecuteOperationOptions + ) => apollo.execute(UpdateRegionDocument, { input }, options) + + beforeEach(async () => { + // Create new region + await createRegion(createdRegionInput, { assertNoErrors: true }) + }) + + afterEach(async () => { + // Wipe created regions + await truncateRegionsSafely() + }) + + it("can't update region if non-admin", async () => { + const res = await updateRegion( + { + key: createdRegionInput.key, + name: 'Updated Region 1' + }, + { + context: { + userId: testBasicUser.id, + role: Roles.Server.User + } + } + ) + expect(res).to.haveGraphQLErrors('You do not have the required server role') + }) + + it('works with valid input', async () => { + const updatedName = 'aaa Updated Region 1' + const updatedDescription = 'bbb Updated description' + + const res = await updateRegion({ + key: createdRegionInput.key, + name: updatedName, + description: updatedDescription + }) + + expect(res.data?.serverInfoMutations.multiRegion.update).to.deep.equal({ + ...createdRegionInput, + id: createdRegionInput.key, + name: updatedName, + description: updatedDescription + }) + expect(res).to.not.haveGraphQLErrors() + }) + + it('fails gracefully with invalid region key', async () => { + const res = await updateRegion({ + key: 'invalid-key', + name: 'Updated Region 1' + }) + + expect(res).to.haveGraphQLErrors('Region not found') + expect(res.data?.serverInfoMutations.multiRegion.update).to.be.not.ok + }) + }) }) }) - }) -}) + : void 0 diff --git a/packages/server/modules/shared/authz.ts b/packages/server/modules/shared/authz.ts index e4eaa9577a..2aa8152a9b 100644 --- a/packages/server/modules/shared/authz.ts +++ b/packages/server/modules/shared/authz.ts @@ -6,8 +6,8 @@ import { ForbiddenError, UnauthorizedError, ContextError, - BadRequestError, - DatabaseError + DatabaseError, + NotFoundError } from '@/modules/shared/errors' import { adminOverrideEnabled } from '@/modules/shared/helpers/envHelper' import { @@ -247,7 +247,7 @@ export const validateRequiredStreamFactory = if (!stream) return authFailed( context, - new BadRequestError('Stream inputs are malformed'), + new NotFoundError('Stream inputs are malformed'), true ) context.stream = stream diff --git a/packages/server/modules/shared/helpers/envHelper.ts b/packages/server/modules/shared/helpers/envHelper.ts index fc220a8135..dec954a07e 100644 --- a/packages/server/modules/shared/helpers/envHelper.ts +++ b/packages/server/modules/shared/helpers/envHelper.ts @@ -53,9 +53,18 @@ export function getBooleanFromEnv(envVarKey: string, aDefault = false): boolean return ['1', 'true', true].includes(process.env[envVarKey] || aDefault.toString()) } -export function getStringFromEnv(envVarKey: string): string { +export function getStringFromEnv( + envVarKey: string, + options?: Partial<{ + /** + * If set to true, wont throw if the env var is not set + */ + unsafe: boolean + }> +): string { const envVar = process.env[envVarKey] if (!envVar) { + if (options?.unsafe) return '' throw new MisconfiguredEnvironmentError(`${envVarKey} env var not configured`) } return envVar @@ -415,8 +424,8 @@ export function getOtelHeaderValue() { return getStringFromEnv('OTEL_TRACE_VALUE') } -export function getMultiRegionConfigPath() { - return getStringFromEnv('MULTI_REGION_CONFIG_PATH') +export function getMultiRegionConfigPath(options?: Partial<{ unsafe: boolean }>) { + return getStringFromEnv('MULTI_REGION_CONFIG_PATH', options) } export const shouldRunTestsInMultiregionMode = () => diff --git a/packages/server/modules/shared/middleware/index.ts b/packages/server/modules/shared/middleware/index.ts index aa5abc274c..f0d2d9c721 100644 --- a/packages/server/modules/shared/middleware/index.ts +++ b/packages/server/modules/shared/middleware/index.ts @@ -6,7 +6,11 @@ import { authHasFailed } from '@/modules/shared/authz' import { Request, Response, NextFunction, Handler } from 'express' -import { ForbiddenError, UnauthorizedError } from '@/modules/shared/errors' +import { + ForbiddenError, + NotFoundError, + UnauthorizedError +} from '@/modules/shared/errors' import { ensureError } from '@/modules/shared/helpers/errorHelper' import { TokenValidationResult } from '@/modules/core/helpers/types' import { buildRequestLoaders } from '@/modules/core/loaders' @@ -54,6 +58,7 @@ export const authMiddlewareCreator = (steps: AuthPipelineFunction[]) => { message = authResult.error?.message || message if (authResult.error instanceof UnauthorizedError) status = 401 if (authResult.error instanceof ForbiddenError) status = 403 + if (authResult.error instanceof NotFoundError) status = 404 } return res.status(status).json({ error: message }) } diff --git a/packages/server/modules/shared/test/authz.spec.js b/packages/server/modules/shared/test/authz.spec.js index d93ff9fae1..5eb88c9719 100644 --- a/packages/server/modules/shared/test/authz.spec.js +++ b/packages/server/modules/shared/test/authz.spec.js @@ -14,9 +14,9 @@ const { const { ForbiddenError: SFE, UnauthorizedError: SUE, - BadRequestError, UnauthorizedError, - ContextError + ContextError, + NotFoundError } = require('@/modules/shared/errors') const { Roles } = require('@speckle/shared') const { @@ -380,7 +380,7 @@ describe('AuthZ @shared', () => { context: {} }) - expectAuthError(new BadRequestError('Stream inputs are malformed'), authResult) + expectAuthError(new NotFoundError('Stream inputs are malformed'), authResult) }) }) describe('Escape hatches', () => { diff --git a/packages/server/modules/workspaces/domain/operations.ts b/packages/server/modules/workspaces/domain/operations.ts index 230b80ce17..774e63bc84 100644 --- a/packages/server/modules/workspaces/domain/operations.ts +++ b/packages/server/modules/workspaces/domain/operations.ts @@ -63,6 +63,11 @@ export type GetWorkspaceBySlug = (args: { userId?: string }) => Promise +// Useful for dev purposes (e.g. CLI) +export type GetWorkspaceBySlugOrId = (args: { + workspaceSlugOrId: string +}) => Promise + export type GetWorkspaces = (args: { workspaceIds: string[] userId?: string diff --git a/packages/server/modules/workspaces/repositories/workspaces.ts b/packages/server/modules/workspaces/repositories/workspaces.ts index dd836280ad..4504017877 100644 --- a/packages/server/modules/workspaces/repositories/workspaces.ts +++ b/packages/server/modules/workspaces/repositories/workspaces.ts @@ -15,6 +15,7 @@ import { GetUserIdsWithRoleInWorkspace, GetWorkspace, GetWorkspaceBySlug, + GetWorkspaceBySlugOrId, GetWorkspaceCollaborators, GetWorkspaceCollaboratorsTotalCount, GetWorkspaceDomains, @@ -146,6 +147,18 @@ export const getWorkspaceFactory = return workspace || null } +export const getWorkspaceBySlugOrIdFactory = + (deps: { db: Knex }): GetWorkspaceBySlugOrId => + async ({ workspaceSlugOrId }) => { + const { db } = deps + const workspace = await workspaceWithRoleBaseQuery({ db }) + .where(Workspaces.col.slug, workspaceSlugOrId) + .orWhere(Workspaces.col.id, workspaceSlugOrId) + .first() + + return workspace || null + } + export const getWorkspaceBySlugFactory = ({ db }: { db: Knex }): GetWorkspaceBySlug => async ({ workspaceSlug, userId }) => { diff --git a/packages/server/modules/workspaces/tests/integration/regions.graph.spec.ts b/packages/server/modules/workspaces/tests/integration/regions.graph.spec.ts index 18456ec886..e68e28d2cc 100644 --- a/packages/server/modules/workspaces/tests/integration/regions.graph.spec.ts +++ b/packages/server/modules/workspaces/tests/integration/regions.graph.spec.ts @@ -1,4 +1,5 @@ import { db } from '@/db/knex' +import { isMultiRegionEnabled } from '@/modules/multiregion/helpers' import { storeRegionFactory } from '@/modules/multiregion/repositories' import { WorkspaceRegions } from '@/modules/workspaces/repositories/regions' import { @@ -20,180 +21,184 @@ import { Roles } from '@speckle/shared' import { expect } from 'chai' const storeRegion = storeRegionFactory({ db }) +const isEnabled = isMultiRegionEnabled() + +isEnabled + ? describe('Workspace regions GQL', () => { + const region1Key = 'us-west-1' + const region2Key = 'eu-west-2' + + let me: BasicTestUser + let otherGuy: BasicTestUser + + const myFirstWorkspace: BasicTestWorkspace = { + id: '', + ownerId: '', + slug: '', + name: 'My first workspace' + } + + let apollo: TestApolloServer + + before(async () => { + MultiRegionDbSelectorMock.mockFunction('getDb', async () => db) + MultiRegionDbSelectorMock.mockFunction('getRegionDb', async () => db) + + await beforeEachContext() + + me = await createTestUser({ role: Roles.Server.Admin }) + otherGuy = await createTestUser({ role: Roles.Server.User }) + + await Promise.all([ + // Create first test workspace + createTestWorkspace(myFirstWorkspace, me), + // Create a couple of test regions + storeRegion({ + region: { + key: region1Key, + name: 'US West 1' + } + }), + storeRegion({ + region: { + key: region2Key, + name: 'EU West 2' + } + }) + ]) + + await Promise.all([ + // Make otherGuy member of my workspace + assignToWorkspace(myFirstWorkspace, otherGuy) + ]) + + apollo = await testApolloServer({ authUserId: me.id }) + }) -describe('Workspace regions GQL', () => { - const region1Key = 'us-west-1' - const region2Key = 'eu-west-2' - - let me: BasicTestUser - let otherGuy: BasicTestUser - - const myFirstWorkspace: BasicTestWorkspace = { - id: '', - ownerId: '', - slug: '', - name: 'My first workspace' - } - - let apollo: TestApolloServer + after(async () => { + MultiRegionDbSelectorMock.resetMockedFunctions() + await truncateRegionsSafely() + }) - before(async () => { - MultiRegionDbSelectorMock.mockFunction('getDb', async () => db) - MultiRegionDbSelectorMock.mockFunction('getRegionDb', async () => db) + describe('when listing', () => { + it("can't list if not workspace admin", async () => { + const res = await apollo.execute( + GetWorkspaceAvailableRegionsDocument, + { workspaceId: myFirstWorkspace.id }, + { authUserId: otherGuy.id } + ) - await beforeEachContext() + expect(res.data?.workspace.availableRegions).to.be.not.ok + expect(res).to.haveGraphQLErrors('You are not authorized') + }) - me = await createTestUser({ role: Roles.Server.Admin }) - otherGuy = await createTestUser({ role: Roles.Server.User }) + it('can list if workspace admin', async () => { + const res = await apollo.execute(GetWorkspaceAvailableRegionsDocument, { + workspaceId: myFirstWorkspace.id + }) - await Promise.all([ - // Create first test workspace - createTestWorkspace(myFirstWorkspace, me), - // Create a couple of test regions - storeRegion({ - region: { - key: region1Key, - name: 'US West 1' - } - }), - storeRegion({ - region: { - key: region2Key, - name: 'EU West 2' - } + expect(res).to.not.haveGraphQLErrors() + expect( + res.data?.workspace.availableRegions.map((r) => r.key) + ).to.deep.equalInAnyOrder([region1Key, region2Key, ...getRegionKeys()]) + }) }) - ]) - - await Promise.all([ - // Make otherGuy member of my workspace - assignToWorkspace(myFirstWorkspace, otherGuy) - ]) - - apollo = await testApolloServer({ authUserId: me.id }) - }) - - after(async () => { - MultiRegionDbSelectorMock.resetMockedFunctions() - await truncateRegionsSafely() - }) - - describe('when listing', () => { - it("can't list if not workspace admin", async () => { - const res = await apollo.execute( - GetWorkspaceAvailableRegionsDocument, - { workspaceId: myFirstWorkspace.id }, - { authUserId: otherGuy.id } - ) - - expect(res.data?.workspace.availableRegions).to.be.not.ok - expect(res).to.haveGraphQLErrors('You are not authorized') - }) - it('can list if workspace admin', async () => { - const res = await apollo.execute(GetWorkspaceAvailableRegionsDocument, { - workspaceId: myFirstWorkspace.id - }) + describe('when setting default region', () => { + const mySecondWorkspace: BasicTestWorkspace = { + id: '', + ownerId: '', + slug: '', + name: 'My second workspace' + } - expect(res).to.not.haveGraphQLErrors() - expect( - res.data?.workspace.availableRegions.map((r) => r.key) - ).to.deep.equalInAnyOrder([region1Key, region2Key, ...getRegionKeys()]) - }) - }) - - describe('when setting default region', () => { - const mySecondWorkspace: BasicTestWorkspace = { - id: '', - ownerId: '', - slug: '', - name: 'My second workspace' - } - - before(async () => { - await createTestWorkspace(mySecondWorkspace, me) - }) + before(async () => { + await createTestWorkspace(mySecondWorkspace, me) + }) - beforeEach(async () => { - await db - .from(WorkspaceRegions.name) - .where({ - [WorkspaceRegions.col.workspaceId]: mySecondWorkspace.id + beforeEach(async () => { + await db + .from(WorkspaceRegions.name) + .where({ + [WorkspaceRegions.col.workspaceId]: mySecondWorkspace.id + }) + .delete() }) - .delete() - }) - it("can't set default region if not workspace admin", async () => { - const res = await apollo.execute( - SetWorkspaceDefaultRegionDocument, - { workspaceId: mySecondWorkspace.id, regionKey: region1Key }, - { authUserId: otherGuy.id } - ) + it("can't set default region if not workspace admin", async () => { + const res = await apollo.execute( + SetWorkspaceDefaultRegionDocument, + { workspaceId: mySecondWorkspace.id, regionKey: region1Key }, + { authUserId: otherGuy.id } + ) - expect(res).to.haveGraphQLErrors('You are not authorized') - expect(res.data?.workspaceMutations.setDefaultRegion).to.be.not.ok - }) + expect(res).to.haveGraphQLErrors('You are not authorized') + expect(res.data?.workspaceMutations.setDefaultRegion).to.be.not.ok + }) + + it('can set default region if workspace admin', async () => { + const res = await apollo.execute(SetWorkspaceDefaultRegionDocument, { + workspaceId: mySecondWorkspace.id, + regionKey: region1Key + }) - it('can set default region if workspace admin', async () => { - const res = await apollo.execute(SetWorkspaceDefaultRegionDocument, { - workspaceId: mySecondWorkspace.id, - regionKey: region1Key + expect(res).to.not.haveGraphQLErrors() + expect( + res.data?.workspaceMutations.setDefaultRegion.defaultRegion?.key + ).to.equal(region1Key) + }) }) - expect(res).to.not.haveGraphQLErrors() - expect(res.data?.workspaceMutations.setDefaultRegion.defaultRegion?.key).to.equal( - region1Key - ) - }) - }) - - describe('with existing default region', () => { - const myThirdWorkspace: BasicTestWorkspace = { - id: '', - ownerId: '', - slug: '', - name: 'My third workspace' - } - - before(async () => { - await createTestWorkspace(myThirdWorkspace, me) - await apollo.execute( - SetWorkspaceDefaultRegionDocument, - { - workspaceId: myThirdWorkspace.id, - regionKey: region1Key - }, - { assertNoErrors: true } - ) - }) + describe('with existing default region', () => { + const myThirdWorkspace: BasicTestWorkspace = { + id: '', + ownerId: '', + slug: '', + name: 'My third workspace' + } - it("can't override default region", async () => { - const res = await apollo.execute(SetWorkspaceDefaultRegionDocument, { - workspaceId: myThirdWorkspace.id, - regionKey: region2Key - }) + before(async () => { + await createTestWorkspace(myThirdWorkspace, me) + await apollo.execute( + SetWorkspaceDefaultRegionDocument, + { + workspaceId: myThirdWorkspace.id, + regionKey: region1Key + }, + { assertNoErrors: true } + ) + }) - expect(res).to.haveGraphQLErrors('Workspace already has a region assigned') - expect(res.data?.workspaceMutations.setDefaultRegion.defaultRegion).to.be.not.ok - }) + it("can't override default region", async () => { + const res = await apollo.execute(SetWorkspaceDefaultRegionDocument, { + workspaceId: myThirdWorkspace.id, + regionKey: region2Key + }) - it('can list default region if workspace admin', async () => { - const res = await apollo.execute(GetWorkspaceDefaultRegionDocument, { - workspaceId: myThirdWorkspace.id - }) + expect(res).to.haveGraphQLErrors('Workspace already has a region assigned') + expect(res.data?.workspaceMutations.setDefaultRegion.defaultRegion).to.be.not + .ok + }) - expect(res).to.not.haveGraphQLErrors() - expect(res.data?.workspace.defaultRegion?.key).to.equal(region1Key) - }) + it('can list default region if workspace admin', async () => { + const res = await apollo.execute(GetWorkspaceDefaultRegionDocument, { + workspaceId: myThirdWorkspace.id + }) - it("can't list default region if not workspace admin", async () => { - const res = await apollo.execute( - GetWorkspaceDefaultRegionDocument, - { workspaceId: myThirdWorkspace.id }, - { authUserId: otherGuy.id } - ) + expect(res).to.not.haveGraphQLErrors() + expect(res.data?.workspace.defaultRegion?.key).to.equal(region1Key) + }) + + it("can't list default region if not workspace admin", async () => { + const res = await apollo.execute( + GetWorkspaceDefaultRegionDocument, + { workspaceId: myThirdWorkspace.id }, + { authUserId: otherGuy.id } + ) - expect(res).to.haveGraphQLErrors('You are not authorized') - expect(res.data?.workspace.defaultRegion).to.be.not.ok + expect(res).to.haveGraphQLErrors('You are not authorized') + expect(res.data?.workspace.defaultRegion).to.be.not.ok + }) + }) }) - }) -}) + : void 0 diff --git a/packages/server/package.json b/packages/server/package.json index cd0c38ffdf..3d063c321d 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -23,7 +23,7 @@ "dev:clean": "yarn build:clean && yarn dev", "dev:server:test": "cross-env DISABLE_NOTIFICATIONS_CONSUMPTION=true NODE_ENV=test LOG_LEVEL=silent LOG_PRETTY=true node ./bin/ts-www", "test": "cross-env NODE_ENV=test LOG_LEVEL=silent LOG_PRETTY=true mocha", - "test:multiregion": "cross-env RUN_TESTS_IN_MULTIREGION_MODE=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:coverage": "cross-env NODE_ENV=test LOG_LEVEL=silent LOG_PRETTY=true nyc --reporter lcov mocha", "test:report": "yarn test:coverage -- --reporter mocha-junit-reporter --reporter-options mochaFile=reports/test-results.xml", "lint": "yarn lint:tsc && yarn lint:eslint", @@ -130,11 +130,11 @@ "@apollo/rover": "^0.23.0", "@bull-board/express": "^4.2.2", "@faker-js/faker": "^8.4.1", - "@graphql-codegen/cli": "^5.0.2", - "@graphql-codegen/typed-document-node": "^5.0.7", - "@graphql-codegen/typescript": "^4.0.7", - "@graphql-codegen/typescript-operations": "^4.2.1", - "@graphql-codegen/typescript-resolvers": "^4.1.0", + "@graphql-codegen/cli": "^5.0.3", + "@graphql-codegen/typed-document-node": "^5.0.11", + "@graphql-codegen/typescript": "^4.1.1", + "@graphql-codegen/typescript-operations": "^4.3.1", + "@graphql-codegen/typescript-resolvers": "^4.4.0", "@parcel/watcher": "^2.4.1", "@swc/core": "^1.2.222", "@tiptap/core": "^2.0.0-beta.176", diff --git a/packages/server/test/hooks.ts b/packages/server/test/hooks.ts index 0a0ce1a97c..495ab17d02 100644 --- a/packages/server/test/hooks.ts +++ b/packages/server/test/hooks.ts @@ -18,7 +18,6 @@ import type express from 'express' import type net from 'net' import { MaybeAsync, MaybeNullOrUndefined } from '@speckle/shared' import type mocha from 'mocha' -import { shouldRunTestsInMultiregionMode } from '@/modules/shared/helpers/envHelper' import { getAvailableRegionKeysFactory, getFreeRegionKeysFactory @@ -36,6 +35,8 @@ import { initializeRegion } from '@/modules/multiregion/dbSelector' import { Knex } from 'knex' +import { isMultiRegionTestMode } from '@/test/speckle-helpers/regions' +import { isMultiRegionEnabled } from '@/modules/multiregion/helpers' // why is server config only created once!???? // because its done in a migration, to not override existing configs @@ -98,8 +99,9 @@ const setupMultiregionMode = async () => { // If not in multi region mode, delete region entries // we only needed them to reset schemas - if (!shouldRunTestsInMultiregionMode()) { + if (!isMultiRegionTestMode()) { await truncateTables([Regions.name]) + regionClients = {} } } @@ -113,7 +115,8 @@ const unlockFactory = (deps: { db: Knex }) => async () => { export const getRegionKeys = () => Object.keys(regionClients) export const resetPubSubFactory = (deps: { db: Knex }) => async () => { - if (!shouldRunTestsInMultiregionMode()) { + // We wanna reset even outside of multiregion test mode, as long as multi region is generally enabled + if (!isMultiRegionEnabled()) { return { drop: async () => {}, reenable: async () => {} } } @@ -138,11 +141,16 @@ export const resetPubSubFactory = (deps: { db: Knex }) => async () => { // Drop all subs for (const sub of subscriptions.rows) { - await deps.db.raw(` - SELECT * FROM aiven_extras.pg_alter_subscription_disable('${sub.subname}'); - SELECT * FROM aiven_extras.pg_drop_subscription('${sub.subname}'); - SELECT * FROM aiven_extras.dblink_slot_create_or_drop('${sub.subconninfo}', '${sub.subslotname}', 'drop'); - `) + // Running serially, otherwise some kind of race condition issue can pop up + await deps.db.raw( + `SELECT * FROM aiven_extras.pg_alter_subscription_disable('${sub.subname}');` + ) + await deps.db.raw( + `SELECT * FROM aiven_extras.pg_drop_subscription('${sub.subname}');` + ) + await deps.db.raw( + `SELECT * FROM aiven_extras.dblink_slot_create_or_drop('${sub.subconninfo}', '${sub.subslotname}', 'drop');` + ) } // Drop all pubs @@ -240,7 +248,7 @@ export const initializeTestServer = async ( export const mochaHooks: mocha.RootHookObject = { beforeAll: async () => { - if (shouldRunTestsInMultiregionMode()) { + if (isMultiRegionTestMode()) { console.log('Running tests in multi-region mode...') } diff --git a/packages/server/test/speckle-helpers/regions.ts b/packages/server/test/speckle-helpers/regions.ts index 33efbecc9e..6a11697892 100644 --- a/packages/server/test/speckle-helpers/regions.ts +++ b/packages/server/test/speckle-helpers/regions.ts @@ -1,5 +1,10 @@ import { db } from '@/db/knex' +import { isMultiRegionEnabled } from '@/modules/multiregion/helpers' import { Regions } from '@/modules/multiregion/repositories' +import { + isTestEnv, + shouldRunTestsInMultiregionMode +} from '@/modules/shared/helpers/envHelper' import { getRegionKeys } from '@/test/hooks' /** @@ -9,3 +14,6 @@ export const truncateRegionsSafely = async () => { const regionKeys = getRegionKeys() await db(Regions.name).whereNotIn(Regions.col.key, regionKeys).delete() } + +export const isMultiRegionTestMode = () => + isMultiRegionEnabled() && isTestEnv() && shouldRunTestsInMultiregionMode() diff --git a/yarn.lock b/yarn.lock index b450ee72aa..ccaa2322c5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10766,6 +10766,60 @@ __metadata: languageName: node linkType: hard +"@graphql-codegen/cli@npm:^5.0.3": + version: 5.0.3 + resolution: "@graphql-codegen/cli@npm:5.0.3" + dependencies: + "@babel/generator": "npm:^7.18.13" + "@babel/template": "npm:^7.18.10" + "@babel/types": "npm:^7.18.13" + "@graphql-codegen/client-preset": "npm:^4.4.0" + "@graphql-codegen/core": "npm:^4.0.2" + "@graphql-codegen/plugin-helpers": "npm:^5.0.3" + "@graphql-tools/apollo-engine-loader": "npm:^8.0.0" + "@graphql-tools/code-file-loader": "npm:^8.0.0" + "@graphql-tools/git-loader": "npm:^8.0.0" + "@graphql-tools/github-loader": "npm:^8.0.0" + "@graphql-tools/graphql-file-loader": "npm:^8.0.0" + "@graphql-tools/json-file-loader": "npm:^8.0.0" + "@graphql-tools/load": "npm:^8.0.0" + "@graphql-tools/prisma-loader": "npm:^8.0.0" + "@graphql-tools/url-loader": "npm:^8.0.0" + "@graphql-tools/utils": "npm:^10.0.0" + "@whatwg-node/fetch": "npm:^0.9.20" + chalk: "npm:^4.1.0" + cosmiconfig: "npm:^8.1.3" + debounce: "npm:^1.2.0" + detect-indent: "npm:^6.0.0" + graphql-config: "npm:^5.1.1" + inquirer: "npm:^8.0.0" + is-glob: "npm:^4.0.1" + jiti: "npm:^1.17.1" + json-to-pretty-yaml: "npm:^1.2.2" + listr2: "npm:^4.0.5" + log-symbols: "npm:^4.0.0" + micromatch: "npm:^4.0.5" + shell-quote: "npm:^1.7.3" + string-env-interpolation: "npm:^1.0.1" + ts-log: "npm:^2.2.3" + tslib: "npm:^2.4.0" + yaml: "npm:^2.3.1" + yargs: "npm:^17.0.0" + peerDependencies: + "@parcel/watcher": ^2.1.0 + graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 + peerDependenciesMeta: + "@parcel/watcher": + optional: true + bin: + gql-gen: cjs/bin.js + graphql-code-generator: cjs/bin.js + graphql-codegen: cjs/bin.js + graphql-codegen-esm: esm/bin.js + checksum: 10/c3359668f824246e78656d26af506b5b279d50e08a56f54db87da492bd4d0a8e8b6540a6119402d7f5026c137babfd79e628897c6038e199ee6322f688eec757 + languageName: node + linkType: hard + "@graphql-codegen/client-preset@npm:^4.2.2, @graphql-codegen/client-preset@npm:^4.3.0": version: 4.3.0 resolution: "@graphql-codegen/client-preset@npm:4.3.0" @@ -10789,6 +10843,29 @@ __metadata: languageName: node linkType: hard +"@graphql-codegen/client-preset@npm:^4.4.0, @graphql-codegen/client-preset@npm:^4.5.0": + version: 4.5.0 + resolution: "@graphql-codegen/client-preset@npm:4.5.0" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.20.2" + "@babel/template": "npm:^7.20.7" + "@graphql-codegen/add": "npm:^5.0.3" + "@graphql-codegen/gql-tag-operations": "npm:4.0.11" + "@graphql-codegen/plugin-helpers": "npm:^5.1.0" + "@graphql-codegen/typed-document-node": "npm:^5.0.11" + "@graphql-codegen/typescript": "npm:^4.1.1" + "@graphql-codegen/typescript-operations": "npm:^4.3.1" + "@graphql-codegen/visitor-plugin-common": "npm:^5.5.0" + "@graphql-tools/documents": "npm:^1.0.0" + "@graphql-tools/utils": "npm:^10.0.0" + "@graphql-typed-document-node/core": "npm:3.2.0" + tslib: "npm:~2.6.0" + peerDependencies: + graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 + checksum: 10/bbbbaa255f6cb1248cd143b54e06f6fc553cdd9f7ca002977bbf42b92cf9d5c6fe052eda1ae1233eab3d50dd80fbb04609bfeeb29132019faead04300e61ddc0 + languageName: node + linkType: hard + "@graphql-codegen/core@npm:^4.0.2": version: 4.0.2 resolution: "@graphql-codegen/core@npm:4.0.2" @@ -10803,6 +10880,21 @@ __metadata: languageName: node linkType: hard +"@graphql-codegen/gql-tag-operations@npm:4.0.11": + version: 4.0.11 + resolution: "@graphql-codegen/gql-tag-operations@npm:4.0.11" + dependencies: + "@graphql-codegen/plugin-helpers": "npm:^5.1.0" + "@graphql-codegen/visitor-plugin-common": "npm:5.5.0" + "@graphql-tools/utils": "npm:^10.0.0" + auto-bind: "npm:~4.0.0" + tslib: "npm:~2.6.0" + peerDependencies: + graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 + checksum: 10/cc277d1af9da611dbd37c00f18d08e8fdc634632c0fba6789a1027931f8e3b925ad64af27a6fa7c23ed44afdef131f9c03025ca9b077cd6e95e5c9823751c6a3 + languageName: node + linkType: hard + "@graphql-codegen/gql-tag-operations@npm:4.0.7": version: 4.0.7 resolution: "@graphql-codegen/gql-tag-operations@npm:4.0.7" @@ -10847,6 +10939,22 @@ __metadata: languageName: node linkType: hard +"@graphql-codegen/plugin-helpers@npm:^5.1.0": + version: 5.1.0 + resolution: "@graphql-codegen/plugin-helpers@npm:5.1.0" + dependencies: + "@graphql-tools/utils": "npm:^10.0.0" + change-case-all: "npm:1.0.15" + common-tags: "npm:1.8.2" + import-from: "npm:4.0.0" + lodash: "npm:~4.17.0" + tslib: "npm:~2.6.0" + peerDependencies: + graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 + checksum: 10/415e79be90a1f5d289c9cd7f0a581c277d544be1f7136d7f74f5f067c205eb35fd6cd522455866fa8105f241eec4c77bebe02eef007d5021a7b7a453b85b2001 + languageName: node + linkType: hard + "@graphql-codegen/schema-ast@npm:^4.0.2": version: 4.0.2 resolution: "@graphql-codegen/schema-ast@npm:4.0.2" @@ -10860,6 +10968,21 @@ __metadata: languageName: node linkType: hard +"@graphql-codegen/typed-document-node@npm:^5.0.11": + version: 5.0.11 + resolution: "@graphql-codegen/typed-document-node@npm:5.0.11" + dependencies: + "@graphql-codegen/plugin-helpers": "npm:^5.1.0" + "@graphql-codegen/visitor-plugin-common": "npm:5.5.0" + auto-bind: "npm:~4.0.0" + change-case-all: "npm:1.0.15" + tslib: "npm:~2.6.0" + peerDependencies: + graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 + checksum: 10/9320fbc9ccf13d0b0ecc7b57f1b0799629ce93a4e0cf95a76cdeb38981e2da92775734daa7bf68a9383e3d01f9a47f4b35cb870aef710f5dc137234b93b9d7cf + languageName: node + linkType: hard + "@graphql-codegen/typed-document-node@npm:^5.0.7": version: 5.0.7 resolution: "@graphql-codegen/typed-document-node@npm:5.0.7" @@ -10904,19 +11027,34 @@ __metadata: languageName: node linkType: hard -"@graphql-codegen/typescript-resolvers@npm:^4.1.0": - version: 4.1.0 - resolution: "@graphql-codegen/typescript-resolvers@npm:4.1.0" +"@graphql-codegen/typescript-operations@npm:^4.3.1": + version: 4.3.1 + resolution: "@graphql-codegen/typescript-operations@npm:4.3.1" dependencies: - "@graphql-codegen/plugin-helpers": "npm:^5.0.4" - "@graphql-codegen/typescript": "npm:^4.0.7" - "@graphql-codegen/visitor-plugin-common": "npm:5.2.0" + "@graphql-codegen/plugin-helpers": "npm:^5.1.0" + "@graphql-codegen/typescript": "npm:^4.1.1" + "@graphql-codegen/visitor-plugin-common": "npm:5.5.0" + auto-bind: "npm:~4.0.0" + tslib: "npm:~2.6.0" + peerDependencies: + graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 + checksum: 10/cdad24e16aa9b369e3ef2434032f2527fd1363e82256dd09d2e9aa6d9a55539eeea15665a4289e7695145f7417a9a765ad73979054a97c606d757ee060780819 + languageName: node + linkType: hard + +"@graphql-codegen/typescript-resolvers@npm:^4.4.0": + version: 4.4.0 + resolution: "@graphql-codegen/typescript-resolvers@npm:4.4.0" + dependencies: + "@graphql-codegen/plugin-helpers": "npm:^5.1.0" + "@graphql-codegen/typescript": "npm:^4.1.1" + "@graphql-codegen/visitor-plugin-common": "npm:5.5.0" "@graphql-tools/utils": "npm:^10.0.0" auto-bind: "npm:~4.0.0" tslib: "npm:~2.6.0" peerDependencies: graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 - checksum: 10/6ec1e225748d41d925bf1c7d71b868cf24a619c7b6e0e86f887a070f5630f0ff932936b3526cf6347304b58e1a0d365f9aff6d1cf3fbc24684b90d5cb6f852ab + checksum: 10/26efe707c89a9612da0ff8be56ebdb91227fe6afb3889b22f49bbec2cd12ba677d58f7946871179dc64c8279acbad773987086fd1c388980c9a7932fd7c15e32 languageName: node linkType: hard @@ -10935,18 +11073,18 @@ __metadata: languageName: node linkType: hard -"@graphql-codegen/typescript@npm:^4.0.9": - version: 4.0.9 - resolution: "@graphql-codegen/typescript@npm:4.0.9" +"@graphql-codegen/typescript@npm:^4.1.1": + version: 4.1.1 + resolution: "@graphql-codegen/typescript@npm:4.1.1" dependencies: - "@graphql-codegen/plugin-helpers": "npm:^5.0.4" + "@graphql-codegen/plugin-helpers": "npm:^5.1.0" "@graphql-codegen/schema-ast": "npm:^4.0.2" - "@graphql-codegen/visitor-plugin-common": "npm:5.3.1" + "@graphql-codegen/visitor-plugin-common": "npm:5.5.0" auto-bind: "npm:~4.0.0" tslib: "npm:~2.6.0" peerDependencies: graphql: ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 - checksum: 10/304026adfe622530b8a2827569dd5bbd390177051be8c214fb79873ec64ef21793635c91657703bfd229a3d06f1a8a6f1addd8ae7eab20d1eff2efe6fb044df7 + checksum: 10/a47fabef00832122f4981fecbbcfd1e90e2567bdc7fc1d63520b018ae1a6db5217eb42f4f4744265cc492e64cd134b87b7bcfdaddfd7b3e35ce5c47d4548225d languageName: node linkType: hard @@ -10970,11 +11108,11 @@ __metadata: languageName: node linkType: hard -"@graphql-codegen/visitor-plugin-common@npm:5.3.1": - version: 5.3.1 - resolution: "@graphql-codegen/visitor-plugin-common@npm:5.3.1" +"@graphql-codegen/visitor-plugin-common@npm:5.5.0, @graphql-codegen/visitor-plugin-common@npm:^5.5.0": + version: 5.5.0 + resolution: "@graphql-codegen/visitor-plugin-common@npm:5.5.0" dependencies: - "@graphql-codegen/plugin-helpers": "npm:^5.0.4" + "@graphql-codegen/plugin-helpers": "npm:^5.1.0" "@graphql-tools/optimize": "npm:^2.0.0" "@graphql-tools/relay-operation-optimizer": "npm:^7.0.0" "@graphql-tools/utils": "npm:^10.0.0" @@ -10986,7 +11124,7 @@ __metadata: tslib: "npm:~2.6.0" peerDependencies: graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 - checksum: 10/6dd0464d9099d5aeabeb766515fc8dd2fc84bcae4cb0e3653d7f38aea716d6622d35d7cbb57a1954e6bc1cde10f4dd8c4a75ceb4e8bb8cdbba16219615666a5f + checksum: 10/f923c40ae996a2accf3a951d302b3da9b3c063f4b1c66b159bf3f74910e18ea592e87b3f35495a84f6c36d1198d880dd07f6e8c3fe94b0d6dba0f2f77522cb5d languageName: node linkType: hard @@ -16611,11 +16749,11 @@ __metadata: "@datadog/browser-rum": "npm:^5.11.0" "@datadog/datadog-ci": "npm:^2.37.0" "@eslint/config-inspector": "npm:^0.4.10" - "@graphql-codegen/cli": "npm:^5.0.2" - "@graphql-codegen/client-preset": "npm:^4.3.0" - "@graphql-codegen/plugin-helpers": "npm:^5.0.4" - "@graphql-codegen/typescript": "npm:^4.0.9" - "@graphql-codegen/visitor-plugin-common": "npm:5.3.1" + "@graphql-codegen/cli": "npm:^5.0.3" + "@graphql-codegen/client-preset": "npm:^4.5.0" + "@graphql-codegen/plugin-helpers": "npm:^5.1.0" + "@graphql-codegen/typescript": "npm:^4.1.1" + "@graphql-codegen/visitor-plugin-common": "npm:5.5.0" "@headlessui/vue": "npm:^1.7.13" "@heroicons/vue": "npm:^2.0.12" "@jsonforms/core": "npm:^3.3.0" @@ -16949,11 +17087,11 @@ __metadata: "@bull-board/express": "npm:^4.2.2" "@faker-js/faker": "npm:^8.4.1" "@godaddy/terminus": "npm:^4.9.0" - "@graphql-codegen/cli": "npm:^5.0.2" - "@graphql-codegen/typed-document-node": "npm:^5.0.7" - "@graphql-codegen/typescript": "npm:^4.0.7" - "@graphql-codegen/typescript-operations": "npm:^4.2.1" - "@graphql-codegen/typescript-resolvers": "npm:^4.1.0" + "@graphql-codegen/cli": "npm:^5.0.3" + "@graphql-codegen/typed-document-node": "npm:^5.0.11" + "@graphql-codegen/typescript": "npm:^4.1.1" + "@graphql-codegen/typescript-operations": "npm:^4.3.1" + "@graphql-codegen/typescript-resolvers": "npm:^4.4.0" "@graphql-tools/mock": "npm:^9.0.4" "@graphql-tools/schema": "npm:^10.0.6" "@lifeomic/attempt": "npm:^3.1.0" @@ -23601,6 +23739,16 @@ __metadata: languageName: node linkType: hard +"@whatwg-node/fetch@npm:^0.9.20": + version: 0.9.23 + resolution: "@whatwg-node/fetch@npm:0.9.23" + dependencies: + "@whatwg-node/node-fetch": "npm:^0.6.0" + urlpattern-polyfill: "npm:^10.0.0" + checksum: 10/6024a3fcc2175de6a20ea4833c009d0488cf68c01cd235541ec0dba0ce59bb0b0befcd4cd788db0e65b99a5a8755bc00d490dc9d7beeb0c2f35058ef46732fe0 + languageName: node + linkType: hard + "@whatwg-node/node-fetch@npm:^0.3.6": version: 0.3.6 resolution: "@whatwg-node/node-fetch@npm:0.3.6" @@ -23627,6 +23775,18 @@ __metadata: languageName: node linkType: hard +"@whatwg-node/node-fetch@npm:^0.6.0": + version: 0.6.0 + resolution: "@whatwg-node/node-fetch@npm:0.6.0" + dependencies: + "@kamilkisiela/fast-url-parser": "npm:^1.1.4" + busboy: "npm:^1.6.0" + fast-querystring: "npm:^1.1.1" + tslib: "npm:^2.6.3" + checksum: 10/87ad7c4cc68b24499089166617d16cbe25d9107b4d9354c804232f8c53c4fc27d1e2166471d878390442620e09588aa1d8705a8e2ea5bcc2d728a558ad1156c3 + languageName: node + linkType: hard + "@wry/caches@npm:^1.0.0": version: 1.0.1 resolution: "@wry/caches@npm:1.0.1" @@ -33572,6 +33732,31 @@ __metadata: languageName: node linkType: hard +"graphql-config@npm:^5.1.1": + version: 5.1.3 + resolution: "graphql-config@npm:5.1.3" + dependencies: + "@graphql-tools/graphql-file-loader": "npm:^8.0.0" + "@graphql-tools/json-file-loader": "npm:^8.0.0" + "@graphql-tools/load": "npm:^8.0.0" + "@graphql-tools/merge": "npm:^9.0.0" + "@graphql-tools/url-loader": "npm:^8.0.0" + "@graphql-tools/utils": "npm:^10.0.0" + cosmiconfig: "npm:^8.1.0" + jiti: "npm:^2.0.0" + minimatch: "npm:^9.0.5" + string-env-interpolation: "npm:^1.0.1" + tslib: "npm:^2.4.0" + peerDependencies: + cosmiconfig-toml-loader: ^1.0.0 + graphql: ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 + peerDependenciesMeta: + cosmiconfig-toml-loader: + optional: true + checksum: 10/9d37f5d424f302808102d118988878be5e4841ba1a06a865cdb9052b24e26eaa9923fb18163bf4f32102d87b3895c53e2ffcdebc1d651f04b56f93f5c38b83c3 + languageName: node + linkType: hard + "graphql-redis-subscriptions@npm:^2.2.2": version: 2.4.2 resolution: "graphql-redis-subscriptions@npm:2.4.2" @@ -36695,6 +36880,15 @@ __metadata: languageName: node linkType: hard +"jiti@npm:^2.0.0": + version: 2.4.0 + resolution: "jiti@npm:2.4.0" + bin: + jiti: lib/jiti-cli.mjs + checksum: 10/10aa999a4f9bccc82b1dab9ebaf4484a8770450883c1bf7fafc07f8fca1e417fd8e7731e651337d1060c9e2ff3f97362dcdfd27e86d1f385db97f4adf7b5a21d + languageName: node + linkType: hard + "jiti@npm:^2.1.2": version: 2.3.3 resolution: "jiti@npm:2.3.3" @@ -39184,6 +39378,15 @@ __metadata: languageName: node linkType: hard +"minimatch@npm:^9.0.5": + version: 9.0.5 + resolution: "minimatch@npm:9.0.5" + dependencies: + brace-expansion: "npm:^2.0.1" + checksum: 10/dd6a8927b063aca6d910b119e1f2df6d2ce7d36eab91de83167dd136bb85e1ebff97b0d3de1cb08bd1f7e018ca170b4962479fefab5b2a69e2ae12cb2edc8348 + languageName: node + linkType: hard + "minimatch@npm:~3.0.3, minimatch@npm:~3.0.4": version: 3.0.8 resolution: "minimatch@npm:3.0.8"