feat(regions): move project branches and commits (#3843)

* feat(regions): repo functions for copying project branches and commits

* chore(regions): wire up move to resolver

* chore(regions): successful basic test of project region change

* fix(regions): sabrina carpenter please please please

* fix(regions): repair multiregion test setup

* chore(regions): appease ts

* chore(multiregion): update test multiregion config

* chore(multiregion): fix test docker config and test

* chore(multiregion): use transaction

* chore(multiregion): maybe this will work

* fix(multiregion): drop subs synchronously

* chore(multiregion): desperate test logs

* chore(multiregion): somehow that worked?

* chore(multiregion): add load-bearing log statement

* chore(multiregion): move services

* fix(multiregion): test drop waits

* chore(regions): fix import

* chore(regions): make test a bit more thorough for good measure

* fix(regions): speed up inserts

* fix(regions): ignore workspace conflict on move
This commit is contained in:
Chuck Driesler
2025-02-13 14:39:23 +00:00
committed by GitHub
parent c1d6036830
commit c382064585
27 changed files with 739 additions and 61 deletions
+9 -1
View File
@@ -598,16 +598,24 @@ jobs:
POSTGRES_PASSWORD: speckle
POSTGRES_USER: speckle
command: -c 'max_connections=1000' -c 'port=5433' -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=5434' -c 'wal_level=logical'
- image: 'minio/minio'
command: server /data --console-address ":9001" --address "0.0.0.0:9000"
- image: 'minio/minio'
command: server /data --console-address ":9021" --address "0.0.0.0:9020"
- image: 'minio/minio'
command: server /data --console-address ":9041" --address "0.0.0.0:9040"
environment:
# Same as test-server:
NODE_ENV: test
DATABASE_URL: 'postgres://speckle:speckle@127.0.0.1:5432/speckle2_test'
PGDATABASE: speckle2_test
POSTGRES_MAX_CONNECTIONS_SERVER: 20
POSTGRES_MAX_CONNECTIONS_SERVER: 50
PGUSER: speckle
SESSION_SECRET: 'keyboard cat'
STRATEGY_LOCAL: 'true'
+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"
}
}
}
}
+28
View File
@@ -34,6 +34,22 @@ services:
ports:
- '127.0.0.1:5401:5432'
postgres-region2:
build:
context: .
dockerfile: utils/postgres/Dockerfile
restart: always
environment:
POSTGRES_DB: speckle
POSTGRES_USER: speckle
POSTGRES_PASSWORD: speckle
volumes:
- postgres-region2-data:/var/lib/postgresql/data/
- ./setup/db/10-docker_postgres_init.sql:/docker-entrypoint-initdb.d/10-docker_postgres_init.sql
- ./setup/db/11-docker_postgres_keycloack_init.sql:/docker-entrypoint-initdb.d/11-docker_postgres_keycloack_init.sql
ports:
- '127.0.0.1:5402:5432'
redis:
image: 'redis:7-alpine'
restart: always
@@ -62,6 +78,16 @@ services:
- '127.0.0.1:9020:9000'
- '127.0.0.1:9021:9001'
minio-region2:
image: 'minio/minio'
command: server /data --console-address ":9001"
restart: always
volumes:
- minio-region2-data:/data
ports:
- '127.0.0.1:9040:9000'
- '127.0.0.1:9041:9001'
# Local OIDC provider for testing
keycloak:
image: quay.io/keycloak/keycloak:25.0
@@ -133,8 +159,10 @@ services:
volumes:
postgres-data:
postgres-region1-data:
postgres-region2-data:
redis-data:
pgadmin-data:
redis_insight-data:
minio-data:
minio-region1-data:
minio-region2-data:
@@ -4587,6 +4587,11 @@ export type WorkspaceProjectInviteCreateInput = {
export type WorkspaceProjectMutations = {
__typename?: 'WorkspaceProjectMutations';
create: Project;
/**
* Update project region and move all regional data to new db.
* TODO: Currently performs all operations synchronously in request, should probably be scheduled.
*/
moveToRegion: Project;
moveToWorkspace: Project;
updateRole: Project;
};
@@ -4597,6 +4602,12 @@ export type WorkspaceProjectMutationsCreateArgs = {
};
export type WorkspaceProjectMutationsMoveToRegionArgs = {
projectId: Scalars['String']['input'];
regionKey: Scalars['String']['input'];
};
export type WorkspaceProjectMutationsMoveToWorkspaceArgs = {
projectId: Scalars['String']['input'];
workspaceId: Scalars['String']['input'];
@@ -8290,6 +8301,7 @@ export type WorkspacePlanFieldArgs = {
}
export type WorkspaceProjectMutationsFieldArgs = {
create: WorkspaceProjectMutationsCreateArgs,
moveToRegion: WorkspaceProjectMutationsMoveToRegionArgs,
moveToWorkspace: WorkspaceProjectMutationsMoveToWorkspaceArgs,
updateRole: WorkspaceProjectMutationsUpdateRoleArgs,
}
@@ -12,3 +12,11 @@ extend type WorkspaceMutations {
"""
setDefaultRegion(workspaceId: String!, regionKey: String!): Workspace!
}
extend type WorkspaceProjectMutations {
"""
Update project region and move all regional data to new db.
TODO: Currently performs all operations synchronously in request, should probably be scheduled.
"""
moveToRegion(projectId: String!, regionKey: String!): Project!
}
@@ -63,7 +63,6 @@ const { getServerInfoFactory } = require('@/modules/core/repositories/server')
const { getEventBus } = require('@/modules/shared/services/eventBus')
let sendRequest
let server
const createAppToken = createAppTokenFactory({
storeApiToken: storeApiTokenFactory({ db }),
@@ -128,7 +127,6 @@ describe('GraphQL @apps-api', () => {
before(async () => {
const ctx = await beforeEachContext()
server = ctx.server
;({ sendRequest } = await initializeTestServer(ctx))
testUser = {
name: 'Dimitrie Stefanescu',
@@ -157,10 +155,6 @@ describe('GraphQL @apps-api', () => {
])}`
})
after(async () => {
await server.close()
})
let testAppId
let testApp
@@ -138,7 +138,6 @@ const expect = chai.expect
let app
let sendRequest
let server
describe('Auth @auth', () => {
describe('Local authN & authZ (token endpoints)', () => {
@@ -160,7 +159,6 @@ describe('Auth @auth', () => {
before(async () => {
const ctx = await beforeEachContext()
server = ctx.server
app = ctx.app
;({ sendRequest } = await initializeTestServer(ctx))
@@ -173,10 +171,6 @@ describe('Auth @auth', () => {
)
})
after(async () => {
await server.close()
})
it('Should register a new user (speckle frontend)', async () => {
await request(app)
.post('/auth/local/register?challenge=test')
@@ -4609,6 +4609,11 @@ export type WorkspaceProjectInviteCreateInput = {
export type WorkspaceProjectMutations = {
__typename?: 'WorkspaceProjectMutations';
create: Project;
/**
* Update project region and move all regional data to new db.
* TODO: Currently performs all operations synchronously in request, should probably be scheduled.
*/
moveToRegion: Project;
moveToWorkspace: Project;
updateRole: Project;
};
@@ -4619,6 +4624,12 @@ export type WorkspaceProjectMutationsCreateArgs = {
};
export type WorkspaceProjectMutationsMoveToRegionArgs = {
projectId: Scalars['String']['input'];
regionKey: Scalars['String']['input'];
};
export type WorkspaceProjectMutationsMoveToWorkspaceArgs = {
projectId: Scalars['String']['input'];
workspaceId: Scalars['String']['input'];
@@ -6943,6 +6954,7 @@ export type WorkspacePlanResolvers<ContextType = GraphQLContext, ParentType exte
export type WorkspaceProjectMutationsResolvers<ContextType = GraphQLContext, ParentType extends ResolversParentTypes['WorkspaceProjectMutations'] = ResolversParentTypes['WorkspaceProjectMutations']> = {
create?: Resolver<ResolversTypes['Project'], ParentType, ContextType, RequireFields<WorkspaceProjectMutationsCreateArgs, 'input'>>;
moveToRegion?: Resolver<ResolversTypes['Project'], ParentType, ContextType, RequireFields<WorkspaceProjectMutationsMoveToRegionArgs, 'projectId' | 'regionKey'>>;
moveToWorkspace?: Resolver<ResolversTypes['Project'], ParentType, ContextType, RequireFields<WorkspaceProjectMutationsMoveToWorkspaceArgs, 'projectId' | 'workspaceId'>>;
updateRole?: Resolver<ResolversTypes['Project'], ParentType, ContextType, RequireFields<WorkspaceProjectMutationsUpdateRoleArgs, 'input'>>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
@@ -4590,6 +4590,11 @@ export type WorkspaceProjectInviteCreateInput = {
export type WorkspaceProjectMutations = {
__typename?: 'WorkspaceProjectMutations';
create: Project;
/**
* Update project region and move all regional data to new db.
* TODO: Currently performs all operations synchronously in request, should probably be scheduled.
*/
moveToRegion: Project;
moveToWorkspace: Project;
updateRole: Project;
};
@@ -4600,6 +4605,12 @@ export type WorkspaceProjectMutationsCreateArgs = {
};
export type WorkspaceProjectMutationsMoveToRegionArgs = {
projectId: Scalars['String']['input'];
regionKey: Scalars['String']['input'];
};
export type WorkspaceProjectMutationsMoveToWorkspaceArgs = {
projectId: Scalars['String']['input'];
workspaceId: Scalars['String']['input'];
@@ -214,7 +214,7 @@ const setUpUserReplication = async ({
try {
await from.public.raw(`CREATE PUBLICATION ${pubName} FOR TABLE users;`)
} catch (err) {
if (!(err instanceof Error))
if (!(err instanceof Error)) {
throw new DatabaseError(
'Could not create publication {pubName} when setting up user replication for region {regionName}',
from.public,
@@ -223,7 +223,16 @@ const setUpUserReplication = async ({
info: { pubName, regionName }
}
)
if (!err.message.includes('already exists')) throw err
}
const errorMessage = err.message
if (
!['already exists', 'violates unique constraint'].some((message) =>
errorMessage.includes(message)
)
)
throw err
}
const fromUrl = new URL(
@@ -23,7 +23,7 @@ export async function* executeBatchedSelect<
>(
selectQuery: Knex.QueryBuilder<TRecord, TResult>,
options?: Partial<BatchedSelectOptions>
): AsyncGenerator<TResult, void, unknown> {
): AsyncGenerator<Awaited<typeof selectQuery>, void, unknown> {
const { batchSize = 100, trx } = options || {}
if (trx) selectQuery.transacting(trx)
@@ -34,7 +34,7 @@ export async function* executeBatchedSelect<
let currentOffset = 0
while (hasMorePages) {
const q = selectQuery.clone().offset(currentOffset)
const results = (await q) as TResult
const results = (await q) as Awaited<typeof selectQuery>
if (!results.length) {
hasMorePages = false
@@ -8,7 +8,6 @@ import {
getTotalUserCountFactory
} from '@/modules/stats/repositories/index'
import { Scopes } from '@speckle/shared'
import { Server } from 'node:http'
import { db } from '@/db/knex'
import {
createCommitByBranchIdFactory,
@@ -194,8 +193,7 @@ describe('Server stats services @stats-services', function () {
})
describe('Server stats api @stats-api', function () {
let server: Server,
sendRequest: Awaited<ReturnType<typeof initializeTestServer>>['sendRequest']
let sendRequest: Awaited<ReturnType<typeof initializeTestServer>>['sendRequest']
const adminUser = {
name: 'Dimitrie',
@@ -233,7 +231,6 @@ describe('Server stats api @stats-api', function () {
before(async function () {
this.timeout(15000)
const ctx = await beforeEachContext()
server = ctx.server
;({ sendRequest } = await initializeTestServer(ctx))
adminUser.id = await createUser(adminUser)
@@ -263,10 +260,6 @@ describe('Server stats api @stats-api', function () {
await seedDb(params)
})
after(async function () {
await server.close()
})
it('Should not get stats if user is not admin', async () => {
const res = await sendRequest(adminUser.badToken, { query: fullQuery })
expect(res.body.errors).to.exist
@@ -2,11 +2,7 @@
const expect = require('chai').expect
const assert = require('assert')
const {
beforeEachContext,
initializeTestServer,
truncateTables
} = require('@/test/hooks')
const { beforeEachContext, initializeTestServer } = require('@/test/hooks')
const { noErrors } = require('@/test/helpers')
const { Scopes, Roles } = require('@speckle/shared')
const {
@@ -26,7 +22,6 @@ const {
deleteWebhookFactory,
dispatchStreamEventFactory
} = require('@/modules/webhooks/services/webhooks')
const { Users, Streams } = require('@/modules/core/dbSchema')
const {
getStreamFactory,
createStreamFactory,
@@ -166,7 +161,7 @@ const createPersonalAccessToken = createPersonalAccessTokenFactory({
describe('Webhooks @webhooks', () => {
const getWebhook = getWebhookByIdFactory({ db })
let server, sendRequest
let sendRequest
const userOne = {
name: 'User',
@@ -191,7 +186,6 @@ describe('Webhooks @webhooks', () => {
before(async () => {
const ctx = await beforeEachContext()
server = ctx.server
;({ sendRequest } = await initializeTestServer(ctx))
userOne.id = await createUser(userOne)
@@ -201,16 +195,6 @@ describe('Webhooks @webhooks', () => {
webhookOne.streamId = streamOne.id
})
after(async () => {
await truncateTables([
Users.name,
Streams.name,
'webhooks_config',
'webhooks_events'
])
await server.close()
})
describe('Create, Read, Update, Delete Webhooks', () => {
it('Should create a webhook', async () => {
webhookOne.id = await createWebhookFactory({
@@ -283,7 +283,7 @@ export type GetAvailableRegions = (params: {
workspaceId: string
}) => Promise<ServerRegion[]>
export type AssignRegion = (params: {
export type AssignWorkspaceRegion = (params: {
workspaceId: string
regionKey: string
}) => Promise<void>
@@ -342,3 +342,24 @@ export type ApproveWorkspaceJoinRequest = (
export type DenyWorkspaceJoinRequest = (
params: Pick<WorkspaceJoinRequest, 'workspaceId' | 'userId'>
) => Promise<boolean>
/**
* Project regions
*/
/**
* Updates project region and moves all regional data to target regional db
*/
export type UpdateProjectRegion = (params: {
projectId: string
regionKey: string
}) => Promise<Stream>
export type CopyWorkspace = (params: { workspaceId: string }) => Promise<string>
export type CopyProjects = (params: { projectIds: string[] }) => Promise<string[]>
export type CopyProjectModels = (params: {
projectIds: string[]
}) => Promise<Record<string, number>>
export type CopyProjectVersions = (params: {
projectIds: string[]
}) => Promise<Record<string, number>>
@@ -5,3 +5,9 @@ export class WorkspaceRegionAssignmentError extends BaseError {
static code = 'WORKSPACE_REGION_ASSIGNMENT_ERROR'
static statusCode = 400
}
export class ProjectRegionAssignmentError extends BaseError {
static defaultMessage = 'Failed to assign region to project'
static code = 'PROJECT_REGION_ASSIGNMENT_ERROR'
static statusCode = 400
}
@@ -2,22 +2,37 @@ import { db } from '@/db/knex'
import { Resolvers } from '@/modules/core/graph/generated/graphql'
import { getWorkspacePlanFactory } from '@/modules/gatekeeper/repositories/billing'
import { canWorkspaceUseRegionsFactory } from '@/modules/gatekeeper/services/featureAuthorization'
import { getDb } from '@/modules/multiregion/utils/dbSelector'
import { getDb, getProjectDbClient } from '@/modules/multiregion/utils/dbSelector'
import { getRegionsFactory } from '@/modules/multiregion/repositories'
import { authorizeResolver } from '@/modules/shared'
import {
getDefaultRegionFactory,
upsertRegionAssignmentFactory
} from '@/modules/workspaces/repositories/regions'
import {
copyProjectModelsFactory,
copyProjectsFactory,
copyProjectVersionsFactory,
copyWorkspaceFactory
} from '@/modules/workspaces/repositories/projectRegions'
import {
getWorkspaceFactory,
upsertWorkspaceFactory
} from '@/modules/workspaces/repositories/workspaces'
import {
assignRegionFactory,
assignWorkspaceRegionFactory,
getAvailableRegionsFactory
} from '@/modules/workspaces/services/regions'
import { updateProjectRegionFactory } from '@/modules/workspaces/services/projectRegions'
import { Roles } from '@speckle/shared'
import { getProjectFactory } from '@/modules/core/repositories/projects'
import { getStreamBranchCountFactory } from '@/modules/core/repositories/branches'
import { getStreamCommitCountFactory } from '@/modules/core/repositories/commits'
import { withTransaction } from '@/modules/shared/helpers/dbHelper'
import { getFeatureFlags, isTestEnv } from '@/modules/shared/helpers/envHelper'
import { WorkspacesNotYetImplementedError } from '@/modules/workspaces/errors/workspace'
const { FF_MOVE_PROJECT_REGION_ENABLED } = getFeatureFlags()
export default {
Workspace: {
@@ -37,7 +52,7 @@ export default {
const regionDb = await getDb({ regionKey: args.regionKey })
const assignRegion = assignRegionFactory({
const assignRegion = assignWorkspaceRegionFactory({
getAvailableRegions: getAvailableRegionsFactory({
getRegions: getRegionsFactory({ db }),
canWorkspaceUseRegions: canWorkspaceUseRegionsFactory({
@@ -53,5 +68,40 @@ export default {
return await ctx.loaders.workspaces!.getWorkspace.load(args.workspaceId)
}
},
WorkspaceProjectMutations: {
moveToRegion: async (_parent, args, context) => {
if (!FF_MOVE_PROJECT_REGION_ENABLED && !isTestEnv()) {
throw new WorkspacesNotYetImplementedError()
}
await authorizeResolver(
context.userId,
args.projectId,
Roles.Stream.Owner,
context.resourceAccessRules
)
const sourceDb = await getProjectDbClient({ projectId: args.projectId })
const targetDb = await (await getDb({ regionKey: args.regionKey })).transaction()
const updateProjectRegion = updateProjectRegionFactory({
getProject: getProjectFactory({ db: sourceDb }),
countProjectModels: getStreamBranchCountFactory({ db: sourceDb }),
countProjectVersions: getStreamCommitCountFactory({ db: sourceDb }),
getAvailableRegions: getAvailableRegionsFactory({
getRegions: getRegionsFactory({ db }),
canWorkspaceUseRegions: canWorkspaceUseRegionsFactory({
getWorkspacePlan: getWorkspacePlanFactory({ db })
})
}),
copyWorkspace: copyWorkspaceFactory({ sourceDb, targetDb }),
copyProjects: copyProjectsFactory({ sourceDb, targetDb }),
copyProjectModels: copyProjectModelsFactory({ sourceDb, targetDb }),
copyProjectVersions: copyProjectVersionsFactory({ sourceDb, targetDb })
})
return await withTransaction(updateProjectRegion(args), targetDb)
}
}
} as Resolvers
@@ -0,0 +1,218 @@
import {
BranchCommits,
Branches,
Commits,
StreamCommits,
StreamFavorites,
Streams,
StreamsMeta
} from '@/modules/core/dbSchema'
import { Branch } from '@/modules/core/domain/branches/types'
import { Commit } from '@/modules/core/domain/commits/types'
import { Stream } from '@/modules/core/domain/streams/types'
import {
BranchCommitRecord,
CommitRecord,
StreamCommitRecord,
StreamFavoriteRecord,
StreamRecord
} from '@/modules/core/helpers/types'
import { executeBatchedSelect } from '@/modules/shared/helpers/dbHelper'
import {
CopyProjectModels,
CopyProjects,
CopyProjectVersions,
CopyWorkspace
} from '@/modules/workspaces/domain/operations'
import { WorkspaceNotFoundError } from '@/modules/workspaces/errors/workspace'
import { Knex } from 'knex'
import { Workspace } from '@/modules/workspacesCore/domain/types'
import { Workspaces } from '@/modules/workspacesCore/helpers/db'
const tables = {
workspaces: (db: Knex) => db<Workspace>(Workspaces.name),
projects: (db: Knex) => db<Stream>(Streams.name),
models: (db: Knex) => db<Branch>(Branches.name),
versions: (db: Knex) => db<Commit>(Commits.name),
branchCommits: (db: Knex) => db<BranchCommitRecord>(BranchCommits.name),
streamCommits: (db: Knex) => db<StreamCommitRecord>(StreamCommits.name),
streamFavorites: (db: Knex) => db<StreamFavoriteRecord>(StreamFavorites.name),
streamsMeta: (db: Knex) => db(StreamsMeta.name)
}
export const copyWorkspaceFactory =
(deps: { sourceDb: Knex; targetDb: Knex }): CopyWorkspace =>
async ({ workspaceId }) => {
const workspace = await tables
.workspaces(deps.sourceDb)
.select('*')
.where({ id: workspaceId })
if (!workspace) {
throw new WorkspaceNotFoundError()
}
await tables
.workspaces(deps.targetDb)
.insert(workspace)
.onConflict(Workspaces.withoutTablePrefix.col.id)
.ignore()
return workspaceId
}
export const copyProjectsFactory =
(deps: { sourceDb: Knex; targetDb: Knex }): CopyProjects =>
async ({ projectIds }) => {
const selectProjects = tables
.projects(deps.sourceDb)
.select('*')
.whereIn(Streams.col.id, projectIds)
const copiedProjectIds: string[] = []
// Copy project record
for await (const projects of executeBatchedSelect(selectProjects)) {
const projectIds = projects.map((project) => project.id)
copiedProjectIds.push(...projectIds)
// Copy `streams` rows to target db
await tables
.projects(deps.targetDb)
.insert(projects)
.onConflict(Streams.withoutTablePrefix.col.id)
.merge(Streams.withoutTablePrefix.cols as (keyof StreamRecord)[])
// Fetch `stream_favorites` rows for projects in batch
const selectStreamFavorites = tables
.streamFavorites(deps.sourceDb)
.select('*')
.whereIn(StreamFavorites.col.streamId, projectIds)
for await (const streamFavorites of executeBatchedSelect(selectStreamFavorites)) {
// Copy `stream_favorites` rows to target db
await tables
.streamFavorites(deps.targetDb)
.insert(streamFavorites)
.onConflict()
.ignore()
}
// Fetch `streams_meta` rows for projects in batch
const selectStreamsMetadata = tables
.streamsMeta(deps.sourceDb)
.select('*')
.whereIn(StreamsMeta.col.streamId, projectIds)
for await (const streamsMetadataBatch of executeBatchedSelect(
selectStreamsMetadata
)) {
// Copy `streams_meta` rows to target db
await tables
.streamsMeta(deps.targetDb)
.insert(streamsMetadataBatch)
.onConflict()
.ignore()
}
}
return copiedProjectIds
}
export const copyProjectModelsFactory =
(deps: { sourceDb: Knex; targetDb: Knex }): CopyProjectModels =>
async ({ projectIds }) => {
const copiedModelCountByProjectId: Record<string, number> = {}
// Fetch `branches` rows for projects in batch
const selectModels = tables
.models(deps.sourceDb)
.select('*')
.whereIn(Branches.col.streamId, projectIds)
for await (const models of executeBatchedSelect(selectModels)) {
// Copy `branches` rows to target db
await tables.models(deps.targetDb).insert(models).onConflict().ignore()
for (const model of models) {
copiedModelCountByProjectId[model.streamId] ??= 0
copiedModelCountByProjectId[model.streamId]++
}
}
return copiedModelCountByProjectId
}
export const copyProjectVersionsFactory =
(deps: { sourceDb: Knex; targetDb: Knex }): CopyProjectVersions =>
async ({ projectIds }) => {
const copiedVersionCountByProjectId: Record<string, number> = {}
const selectVersions = tables
.streamCommits(deps.sourceDb)
.select('*')
.join<StreamCommitRecord & Commit>(
Commits.name,
Commits.col.id,
StreamCommits.col.commitId
)
.whereIn(StreamCommits.col.streamId, projectIds)
for await (const versions of executeBatchedSelect(selectVersions)) {
const { commitIds, commits } = versions.reduce(
(all, version) => {
const { commitId, streamId, ...commit } = version
all.commitIds.push(commitId)
all.streamIds.push(streamId)
all.commits.push(commit)
return all
},
{ commitIds: [], streamIds: [], commits: [] } as {
commitIds: string[]
streamIds: string[]
commits: CommitRecord[]
}
)
// Copy `commits` rows to target db
await tables.versions(deps.targetDb).insert(commits).onConflict().ignore()
for (const version of versions) {
copiedVersionCountByProjectId[version.streamId] ??= 0
copiedVersionCountByProjectId[version.streamId]++
}
// Fetch `branch_commits` rows for versions in batch
const selectBranchCommits = tables
.branchCommits(deps.sourceDb)
.select('*')
.whereIn(BranchCommits.col.commitId, commitIds)
for await (const branchCommits of executeBatchedSelect(selectBranchCommits)) {
// Copy `branch_commits` row to target db
await tables
.branchCommits(deps.targetDb)
.insert(branchCommits)
.onConflict()
.ignore()
}
// Fetch `stream_commits` rows for versions in batch
const selectStreamCommits = tables
.streamCommits(deps.sourceDb)
.select('*')
.whereIn(StreamCommits.col.commitId, commitIds)
for await (const streamCommits of executeBatchedSelect(selectStreamCommits)) {
// Copy `stream_commits` row to target db
await tables
.streamCommits(deps.targetDb)
.insert(streamCommits)
.onConflict()
.ignore()
}
}
return copiedVersionCountByProjectId
}
@@ -14,8 +14,8 @@ export const WorkspaceRegions = buildTableHelper('workspace_regions', [
])
const tables = {
workspaceRegions: (db: Knex) => db<WorkspaceRegionAssignment>(WorkspaceRegions.name),
regions: (db: Knex) => db<RegionRecord>(Regions.name)
regions: (db: Knex) => db<RegionRecord>(Regions.name),
workspaceRegions: (db: Knex) => db<WorkspaceRegionAssignment>(WorkspaceRegions.name)
}
export const upsertRegionAssignmentFactory =
@@ -0,0 +1,89 @@
import { GetStreamBranchCount } from '@/modules/core/domain/branches/operations'
import { GetStreamCommitCount } from '@/modules/core/domain/commits/operations'
import { GetProject } from '@/modules/core/domain/projects/operations'
import {
CopyProjectModels,
CopyProjects,
CopyProjectVersions,
CopyWorkspace,
GetAvailableRegions,
UpdateProjectRegion
} from '@/modules/workspaces/domain/operations'
import { ProjectRegionAssignmentError } from '@/modules/workspaces/errors/regions'
export const updateProjectRegionFactory =
(deps: {
getProject: GetProject
countProjectModels: GetStreamBranchCount
countProjectVersions: GetStreamCommitCount
getAvailableRegions: GetAvailableRegions
copyWorkspace: CopyWorkspace
copyProjects: CopyProjects
copyProjectModels: CopyProjectModels
copyProjectVersions: CopyProjectVersions
}): UpdateProjectRegion =>
async (params) => {
const { projectId, regionKey } = params
const project = await deps.getProject({ projectId })
if (!project) {
throw new ProjectRegionAssignmentError('Project not found', {
info: { params }
})
}
if (!project.workspaceId) {
throw new ProjectRegionAssignmentError('Project not a part of a workspace', {
info: { params }
})
}
const availableRegions = await deps.getAvailableRegions({
workspaceId: project.workspaceId
})
if (!availableRegions.find((region) => region.key === regionKey)) {
throw new ProjectRegionAssignmentError(
'Specified region not available for workspace',
{
info: {
params,
workspaceId: project.workspaceId
}
}
)
}
// Move workspace
await deps.copyWorkspace({ workspaceId: project.workspaceId })
// Move commits
const projectIds = await deps.copyProjects({ projectIds: [projectId] })
const modelIds = await deps.copyProjectModels({ projectIds })
const versionIds = await deps.copyProjectVersions({ projectIds })
// TODO: Move objects
// TODO: Move automations
// TODO: Move comments
// TODO: Move file blobs
// TODO: Move webhooks
// TODO: Validate state after move captures latest state of project
const sourceProjectModelCount = await deps.countProjectModels(projectId)
const sourceProjectVersionCount = await deps.countProjectVersions(projectId)
const tests = [
modelIds[projectId] === sourceProjectModelCount,
versionIds[projectId] === sourceProjectVersionCount
]
const isReconciled = tests.every((test) => !!test)
if (!isReconciled) {
// TODO: Move failed or source project added data while changing regions. Retry move.
throw new ProjectRegionAssignmentError(
'Missing data from source project in target region copy after move.'
)
}
// TODO: Update project region in db
return { ...project, regionKey }
}
@@ -1,7 +1,7 @@
import { WorkspaceFeatureAccessFunction } from '@/modules/gatekeeper/domain/operations'
import { GetRegions } from '@/modules/multiregion/domain/operations'
import {
AssignRegion,
AssignWorkspaceRegion,
GetAvailableRegions,
GetDefaultRegion,
GetWorkspace,
@@ -25,14 +25,14 @@ export const getAvailableRegionsFactory =
return await deps.getRegions()
}
export const assignRegionFactory =
export const assignWorkspaceRegionFactory =
(deps: {
getAvailableRegions: GetAvailableRegions
upsertRegionAssignment: UpsertRegionAssignment
getDefaultRegion: GetDefaultRegion
getWorkspace: GetWorkspace
insertRegionWorkspace: UpsertWorkspace
}): AssignRegion =>
}): AssignWorkspaceRegion =>
async (params) => {
const { workspaceId, regionKey } = params
@@ -64,7 +64,7 @@ import {
import { SetOptional } from 'type-fest'
import { isMultiRegionTestMode } from '@/test/speckle-helpers/regions'
import {
assignRegionFactory,
assignWorkspaceRegionFactory,
getAvailableRegionsFactory
} from '@/modules/workspaces/services/regions'
import { getRegionsFactory } from '@/modules/multiregion/repositories'
@@ -184,7 +184,7 @@ export const createTestWorkspace = async (
if (useRegion) {
const regionDb = await getDb({ regionKey })
const assignRegion = assignRegionFactory({
const assignRegion = assignWorkspaceRegionFactory({
getAvailableRegions: getAvailableRegionsFactory({
getRegions: getRegionsFactory({ db }),
canWorkspaceUseRegions: canWorkspaceUseRegionsFactory({
@@ -1,6 +1,15 @@
import { db } from '@/db/knex'
import { AllScopes } from '@/modules/core/helpers/mainConstants'
import { createRandomEmail } from '@/modules/core/helpers/testHelpers'
import {
BranchCommitRecord,
BranchRecord,
CommitRecord,
StreamCommitRecord,
StreamRecord
} from '@/modules/core/helpers/types'
import { grantStreamPermissionsFactory } from '@/modules/core/repositories/streams'
import { getDb } from '@/modules/multiregion/utils/dbSelector'
import {
BasicTestWorkspace,
createTestWorkspace
@@ -8,6 +17,7 @@ import {
import {
BasicTestUser,
createAuthTokenForUser,
createTestUser,
createTestUsers
} from '@/test/authHelper'
import {
@@ -15,7 +25,8 @@ import {
CreateWorkspaceProjectDocument,
GetWorkspaceProjectsDocument,
GetWorkspaceTeamDocument,
MoveProjectToWorkspaceDocument
MoveProjectToWorkspaceDocument,
UpdateProjectRegionDocument
} from '@/test/graphql/generated/graphql'
import {
createTestContext,
@@ -23,10 +34,22 @@ import {
TestApolloServer
} from '@/test/graphqlHelper'
import { beforeEachContext } from '@/test/hooks'
import { BasicTestBranch, createTestBranch } from '@/test/speckle-helpers/branchHelper'
import {
BasicTestCommit,
createTestCommit,
createTestObject
} from '@/test/speckle-helpers/commitHelper'
import {
isMultiRegionTestMode,
waitForRegionUser
} from '@/test/speckle-helpers/regions'
import { BasicTestStream, createTestStream } from '@/test/speckle-helpers/streamHelper'
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 grantStreamPermissions = grantStreamPermissionsFactory({ db })
@@ -272,3 +295,152 @@ describe('Workspace project GQL CRUD', () => {
})
})
})
isMultiRegionTestMode()
? describe('Workspace project region changes', () => {
const regionKey1 = 'region1'
const regionKey2 = 'region2'
const adminUser: BasicTestUser = {
id: '',
name: 'John Speckle',
email: createRandomEmail()
}
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 apollo: TestApolloServer
let targetRegionDb: Knex
before(async () => {
await createTestUser(adminUser)
await waitForRegionUser(adminUser)
apollo = await testApolloServer({ authUserId: adminUser.id })
targetRegionDb = await getDb({ regionKey: regionKey2 })
})
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
})
})
it('moves project record to target regional db', async () => {
const res = await apollo.execute(UpdateProjectRegionDocument, {
projectId: testProject.id,
regionKey: regionKey2
})
expect(res).to.not.haveGraphQLErrors()
// TODO: Replace with gql query when possible
const project = await targetRegionDb
.table<StreamRecord>('streams')
.select('*')
.where({ id: testProject.id })
.first()
expect(project).to.not.be.undefined
})
it('moves project models to target regional db', async () => {
const res = await apollo.execute(UpdateProjectRegionDocument, {
projectId: testProject.id,
regionKey: regionKey2
})
expect(res).to.not.haveGraphQLErrors()
// TODO: Replace with gql query when possible
const branch = await targetRegionDb
.table<BranchRecord>('branches')
.select('*')
.where({ id: testModel.id })
.first()
expect(branch).to.not.be.undefined
})
it('moves project model versions to target regional db', async () => {
const res = await apollo.execute(UpdateProjectRegionDocument, {
projectId: testProject.id,
regionKey: regionKey2
})
expect(res).to.not.haveGraphQLErrors()
// TODO: Replace with gql query when possible
const version = await targetRegionDb
.table<CommitRecord>('commits')
.select('*')
.where({ id: testVersion.id })
.first()
expect(version).to.not.be.undefined
// TODO: Replace with gql query when possible
const streamCommitsRecord = await targetRegionDb
.table<StreamCommitRecord>('stream_commits')
.select('*')
.where({ commitId: testVersion.id })
.first()
expect(streamCommitsRecord).to.not.be.undefined
// TODO: Replace with gql query when possible
const branchCommitsRecord = await targetRegionDb
.table<BranchCommitRecord>('branch_commits')
.select('*')
.where({ commitId: testVersion.id })
.first()
expect(branchCommitsRecord).to.not.be.undefined
})
})
: void 0
@@ -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"
}
}
}
}
@@ -4591,6 +4591,11 @@ export type WorkspaceProjectInviteCreateInput = {
export type WorkspaceProjectMutations = {
__typename?: 'WorkspaceProjectMutations';
create: Project;
/**
* Update project region and move all regional data to new db.
* TODO: Currently performs all operations synchronously in request, should probably be scheduled.
*/
moveToRegion: Project;
moveToWorkspace: Project;
updateRole: Project;
};
@@ -4601,6 +4606,12 @@ export type WorkspaceProjectMutationsCreateArgs = {
};
export type WorkspaceProjectMutationsMoveToRegionArgs = {
projectId: Scalars['String']['input'];
regionKey: Scalars['String']['input'];
};
export type WorkspaceProjectMutationsMoveToWorkspaceArgs = {
projectId: Scalars['String']['input'];
workspaceId: Scalars['String']['input'];
@@ -5209,6 +5220,14 @@ export type UpdateRegionMutationVariables = Exact<{
export type UpdateRegionMutation = { __typename?: 'Mutation', serverInfoMutations: { __typename?: 'ServerInfoMutations', multiRegion: { __typename?: 'ServerRegionMutations', update: { __typename?: 'ServerRegionItem', id: string, key: string, name: string, description?: string | null } } } };
export type UpdateProjectRegionMutationVariables = Exact<{
projectId: Scalars['String']['input'];
regionKey: Scalars['String']['input'];
}>;
export type UpdateProjectRegionMutation = { __typename?: 'Mutation', workspaceMutations: { __typename?: 'WorkspaceMutations', projects: { __typename?: 'WorkspaceProjectMutations', moveToRegion: { __typename?: 'Project', id: string } } } };
export type BasicProjectAccessRequestFieldsFragment = { __typename?: 'ProjectAccessRequest', id: string, requesterId: string, projectId: string, createdAt: string, requester: { __typename?: 'LimitedUser', id: string, name: string } };
export type CreateProjectAccessRequestMutationVariables = Exact<{
@@ -5751,6 +5770,7 @@ export const GetAvailableRegionKeysDocument = {"kind":"Document","definitions":[
export const CreateNewRegionDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateNewRegion"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"CreateServerRegionInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"serverInfoMutations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"multiRegion"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"create"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"MainRegionMetadata"}}]}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"MainRegionMetadata"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ServerRegionItem"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}}]}}]} as unknown as DocumentNode<CreateNewRegionMutation, CreateNewRegionMutationVariables>;
export const GetRegionsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetRegions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"serverInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"multiRegion"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"regions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"MainRegionMetadata"}}]}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"MainRegionMetadata"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ServerRegionItem"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}}]}}]} as unknown as DocumentNode<GetRegionsQuery, GetRegionsQueryVariables>;
export const UpdateRegionDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateRegion"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UpdateServerRegionInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"serverInfoMutations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"multiRegion"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"update"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"MainRegionMetadata"}}]}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"MainRegionMetadata"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ServerRegionItem"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}}]}}]} as unknown as DocumentNode<UpdateRegionMutation, UpdateRegionMutationVariables>;
export const UpdateProjectRegionDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateProjectRegion"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"projectId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"regionKey"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workspaceMutations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"projects"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"moveToRegion"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"projectId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"projectId"}}},{"kind":"Argument","name":{"kind":"Name","value":"regionKey"},"value":{"kind":"Variable","name":{"kind":"Name","value":"regionKey"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]}}]}}]} as unknown as DocumentNode<UpdateProjectRegionMutation, UpdateProjectRegionMutationVariables>;
export const CreateProjectAccessRequestDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateProjectAccessRequest"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"projectId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"projectMutations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"accessRequestMutations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"create"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"projectId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"projectId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"BasicProjectAccessRequestFields"}}]}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"BasicProjectAccessRequestFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ProjectAccessRequest"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"requester"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"requesterId"}},{"kind":"Field","name":{"kind":"Name","value":"projectId"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}}]}}]} as unknown as DocumentNode<CreateProjectAccessRequestMutation, CreateProjectAccessRequestMutationVariables>;
export const GetActiveUserProjectAccessRequestDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetActiveUserProjectAccessRequest"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"projectId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"activeUser"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"projectAccessRequest"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"projectId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"projectId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"BasicProjectAccessRequestFields"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"BasicProjectAccessRequestFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ProjectAccessRequest"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"requester"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"requesterId"}},{"kind":"Field","name":{"kind":"Name","value":"projectId"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}}]}}]} as unknown as DocumentNode<GetActiveUserProjectAccessRequestQuery, GetActiveUserProjectAccessRequestQueryVariables>;
export const GetActiveUserFullProjectAccessRequestDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetActiveUserFullProjectAccessRequest"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"projectId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"activeUser"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"projectAccessRequest"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"projectId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"projectId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"BasicProjectAccessRequestFields"}},{"kind":"Field","name":{"kind":"Name","value":"project"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"BasicProjectAccessRequestFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ProjectAccessRequest"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"requester"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"requesterId"}},{"kind":"Field","name":{"kind":"Name","value":"projectId"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}}]}}]} as unknown as DocumentNode<GetActiveUserFullProjectAccessRequestQuery, GetActiveUserFullProjectAccessRequestQueryVariables>;
@@ -60,3 +60,15 @@ export const updateRegionMutation = gql`
${mainRegionMetadataFragment}
`
export const updateProjectRegionMutation = gql`
mutation UpdateProjectRegion($projectId: String!, $regionKey: String!) {
workspaceMutations {
projects {
moveToRegion(projectId: $projectId, regionKey: $regionKey) {
id
}
}
}
}
`
+4 -6
View File
@@ -22,8 +22,7 @@ import {
MaybeAsync,
MaybeNullOrUndefined,
Nullable,
Optional,
wait
Optional
} from '@speckle/shared'
import * as mocha from 'mocha'
import {
@@ -199,19 +198,18 @@ export const resetPubSubFactory = (deps: { db: Knex }) => async () => {
await deps.db.raw(
`SELECT * FROM aiven_extras.pg_alter_subscription_disable('${info.subname}');`
)
await wait(500)
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');`
)
}
// Drop all subs
// (concurrently, cause it seems possible and we have those delays there)
await Promise.all(subscriptions.rows.map(dropSubs))
for (const sub of subscriptions.rows) {
await dropSubs(sub)
}
// Drop all pubs
for (const pub of publications.rows) {
+12
View File
@@ -60,6 +60,16 @@ const parseFeatureFlags = () => {
FF_FORCE_ONBOARDING: {
schema: z.boolean(),
defaults: { production: false, _: false }
},
// Fixes the streaming of objects by ensuring that the database stream is closed properly
FF_OBJECTS_STREAMING_FIX: {
schema: z.boolean(),
defaults: { production: false, _: false }
},
// Enables endpoint(s) for updating a project's region
FF_MOVE_PROJECT_REGION_ENABLED: {
schema: z.boolean(),
defaults: { production: false, _: true }
}
})
@@ -86,6 +96,8 @@ export function getFeatureFlags(): {
FF_FILEIMPORT_IFC_DOTNET_ENABLED: boolean
FF_FORCE_EMAIL_VERIFICATION: boolean
FF_FORCE_ONBOARDING: boolean
FF_OBJECTS_STREAMING_FIX: boolean
FF_MOVE_PROJECT_REGION_ENABLED: boolean
} {
if (!parsedFlags) parsedFlags = parseFeatureFlags()
return parsedFlags