chore(server): various useful multiregion changes from comments pr (#3499)

* chore(server): various useful multiregion changes from comments pr

* hopefully fixing CI?

* test fix

* more test fixes??

* more test fixes..

* plz work ;((((

* revert multiregion on by default
This commit is contained in:
Kristaps Fabians Geikins 2024-11-15 13:49:34 +00:00 коммит произвёл GitHub
Родитель 1d1a54e082
Коммит f961a6da81
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
25 изменённых файлов: 827 добавлений и 463 удалений

Просмотреть файл

@ -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:

Просмотреть файл

@ -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",

Просмотреть файл

@ -24,6 +24,7 @@ export default knexInstance
export {
knexInstance as db,
knexInstance as knex,
knexInstance as knexInstance,
knexInstance as mainDb,
knexInstance,
config
}

Просмотреть файл

@ -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

Просмотреть файл

@ -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 <workspaceSlugOrId> [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

Просмотреть файл

@ -411,7 +411,7 @@ export const getPaginatedCommitCommentsTotalCountFactory =
(deps: { db: Knex }): GetPaginatedCommitCommentsTotalCount =>
async (params: Omit<PaginatedCommitCommentsParams, 'limit' | 'cursor'>) => {
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<PaginatedBranchCommentsParams, 'limit' | 'cursor'>) => {
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

Просмотреть файл

@ -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

Просмотреть файл

@ -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

Просмотреть файл

@ -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))
}

Просмотреть файл

@ -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')

Просмотреть файл

@ -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) {

Просмотреть файл

@ -19,6 +19,11 @@ export type PaidWorkspacePlanStatuses =
export type TrialWorkspacePlanStatuses = 'trial' | 'expired'
export type PlanStatuses =
| PaidWorkspacePlanStatuses
| TrialWorkspacePlanStatuses
| UnpaidWorkspacePlanStatuses
type BaseWorkspacePlan = {
workspaceId: string
}

Просмотреть файл

@ -17,13 +17,15 @@ import {
let multiRegionConfig: Optional<MultiRegionConfig> = undefined
const getMultiRegionConfig = async (): Promise<MultiRegionConfig> => {
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)

Просмотреть файл

@ -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'
}
}
}
const fakeRegionKey1 = 'us-west-1'
const fakeRegionKey2 = 'eu-east-2'
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
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')
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])
})
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 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
)
expect(res).to.haveGraphQLErrors('You do not have the required server role')
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 })
})
it('allows retrieving all regions', async () => {
const res = await apollo.execute(GetRegionsDocument, {})
after(() => {
MultiRegionConfigMock.resetMockedFunctions()
MultiRegionDbSelectorMock.resetMockedFunctions()
})
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
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 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 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'
}
])
})
it('filters out used region from available keys', async () => {
const res = await apollo.execute(GetAvailableRegionKeysDocument, {})
before(async () => {
// Create a region
await createRegion(createdRegionInput, { assertNoErrors: true })
})
expect(res).to.not.haveGraphQLErrors()
expect(res.data?.serverInfo.multiRegion.availableKeys).to.deep.equal([
fakeRegionKey2
])
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
])
})
})
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
})
})
})
})
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 truncateTables([Regions.name])
})
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

Просмотреть файл

@ -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

Просмотреть файл

@ -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 = () =>

Просмотреть файл

@ -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 })
}

Просмотреть файл

@ -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', () => {

Просмотреть файл

@ -63,6 +63,11 @@ export type GetWorkspaceBySlug = (args: {
userId?: string
}) => Promise<WorkspaceWithOptionalRole | null>
// Useful for dev purposes (e.g. CLI)
export type GetWorkspaceBySlugOrId = (args: {
workspaceSlugOrId: string
}) => Promise<Workspace | null>
export type GetWorkspaces = (args: {
workspaceIds: string[]
userId?: string

Просмотреть файл

@ -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 }) => {

Просмотреть файл

@ -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()
describe('Workspace regions GQL', () => {
const region1Key = 'us-west-1'
const region2Key = 'eu-west-2'
isEnabled
? describe('Workspace regions GQL', () => {
const region1Key = 'us-west-1'
const region2Key = 'eu-west-2'
let me: BasicTestUser
let otherGuy: BasicTestUser
let me: BasicTestUser
let otherGuy: BasicTestUser
const myFirstWorkspace: BasicTestWorkspace = {
id: '',
ownerId: '',
slug: '',
name: 'My first workspace'
}
const myFirstWorkspace: BasicTestWorkspace = {
id: '',
ownerId: '',
slug: '',
name: 'My first workspace'
}
let apollo: TestApolloServer
let apollo: TestApolloServer
before(async () => {
MultiRegionDbSelectorMock.mockFunction('getDb', async () => db)
MultiRegionDbSelectorMock.mockFunction('getRegionDb', async () => db)
before(async () => {
MultiRegionDbSelectorMock.mockFunction('getDb', async () => db)
MultiRegionDbSelectorMock.mockFunction('getRegionDb', async () => db)
await beforeEachContext()
await beforeEachContext()
me = await createTestUser({ role: Roles.Server.Admin })
otherGuy = await createTestUser({ role: Roles.Server.User })
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([
// 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)
])
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
apollo = await testApolloServer({ authUserId: me.id })
})
expect(res).to.not.haveGraphQLErrors()
expect(
res.data?.workspace.availableRegions.map((r) => r.key)
).to.deep.equalInAnyOrder([region1Key, region2Key, ...getRegionKeys()])
})
})
after(async () => {
MultiRegionDbSelectorMock.resetMockedFunctions()
await truncateRegionsSafely()
})
describe('when setting default region', () => {
const mySecondWorkspace: BasicTestWorkspace = {
id: '',
ownerId: '',
slug: '',
name: 'My second workspace'
}
describe('when listing', () => {
it("can't list if not workspace admin", async () => {
const res = await apollo.execute(
GetWorkspaceAvailableRegionsDocument,
{ workspaceId: myFirstWorkspace.id },
{ authUserId: otherGuy.id }
)
before(async () => {
await createTestWorkspace(mySecondWorkspace, me)
})
beforeEach(async () => {
await db
.from(WorkspaceRegions.name)
.where({
[WorkspaceRegions.col.workspaceId]: mySecondWorkspace.id
expect(res.data?.workspace.availableRegions).to.be.not.ok
expect(res).to.haveGraphQLErrors('You are not authorized')
})
.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 list if workspace admin', async () => {
const res = await apollo.execute(GetWorkspaceAvailableRegionsDocument, {
workspaceId: myFirstWorkspace.id
})
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
expect(res).to.not.haveGraphQLErrors()
expect(
res.data?.workspace.availableRegions.map((r) => r.key)
).to.deep.equalInAnyOrder([region1Key, region2Key, ...getRegionKeys()])
})
})
expect(res).to.not.haveGraphQLErrors()
expect(res.data?.workspaceMutations.setDefaultRegion.defaultRegion?.key).to.equal(
region1Key
)
})
})
describe('when setting default region', () => {
const mySecondWorkspace: BasicTestWorkspace = {
id: '',
ownerId: '',
slug: '',
name: 'My second workspace'
}
describe('with existing default region', () => {
const myThirdWorkspace: BasicTestWorkspace = {
id: '',
ownerId: '',
slug: '',
name: 'My third workspace'
}
before(async () => {
await createTestWorkspace(mySecondWorkspace, me)
})
before(async () => {
await createTestWorkspace(myThirdWorkspace, me)
await apollo.execute(
SetWorkspaceDefaultRegionDocument,
{
workspaceId: myThirdWorkspace.id,
regionKey: region1Key
},
{ assertNoErrors: true }
)
})
beforeEach(async () => {
await db
.from(WorkspaceRegions.name)
.where({
[WorkspaceRegions.col.workspaceId]: mySecondWorkspace.id
})
.delete()
})
it("can't override default region", async () => {
const res = await apollo.execute(SetWorkspaceDefaultRegionDocument, {
workspaceId: myThirdWorkspace.id,
regionKey: region2Key
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
})
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.haveGraphQLErrors('Workspace already has a region assigned')
expect(res.data?.workspaceMutations.setDefaultRegion.defaultRegion).to.be.not.ok
})
describe('with existing default region', () => {
const myThirdWorkspace: BasicTestWorkspace = {
id: '',
ownerId: '',
slug: '',
name: 'My third workspace'
}
it('can list default region if workspace admin', async () => {
const res = await apollo.execute(GetWorkspaceDefaultRegionDocument, {
workspaceId: myThirdWorkspace.id
before(async () => {
await createTestWorkspace(myThirdWorkspace, me)
await apollo.execute(
SetWorkspaceDefaultRegionDocument,
{
workspaceId: myThirdWorkspace.id,
regionKey: region1Key
},
{ assertNoErrors: true }
)
})
it("can't override default region", async () => {
const res = await apollo.execute(SetWorkspaceDefaultRegionDocument, {
workspaceId: myThirdWorkspace.id,
regionKey: region2Key
})
expect(res).to.haveGraphQLErrors('Workspace already has a region assigned')
expect(res.data?.workspaceMutations.setDefaultRegion.defaultRegion).to.be.not
.ok
})
it('can list default region if workspace admin', async () => {
const res = await apollo.execute(GetWorkspaceDefaultRegionDocument, {
workspaceId: myThirdWorkspace.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.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
})
})
})
: void 0

Просмотреть файл

@ -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",

Просмотреть файл

@ -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...')
}

Просмотреть файл

@ -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()

259
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"