Merge branch 'main' of github.com:specklesystems/speckle-server into alessandro/web-2304-expose-graphql-field-readonly-for-workspace-type

This commit is contained in:
Alessandro Magionami
2024-12-12 09:41:03 +01:00
147 changed files with 2316 additions and 1131 deletions
@@ -105,6 +105,18 @@ export type GetWorkspaceWithDomains = (args: {
export type DeleteWorkspace = (args: DeleteWorkspaceArgs) => Promise<void>
type CountWorkspacesArgs = {
filter?: {
search?: string
}
}
export type QueryWorkspacesArgs = CountWorkspacesArgs & {
limit: number
cursor?: string
}
export type QueryWorkspaces = (args: QueryWorkspacesArgs) => Promise<Workspace[]>
export type CountWorkspaces = (args: CountWorkspacesArgs) => Promise<number>
/** Workspace Roles */
export type GetWorkspaceCollaboratorsArgs = {
@@ -1,11 +0,0 @@
import { initializeModuleEventEmitter } from '@/modules/shared/services/moduleEventEmitterSetup'
import {
WorkspaceEvents,
WorkspaceEventsPayloads
} from '@/modules/workspacesCore/domain/events'
const { emit, listen } = initializeModuleEventEmitter<WorkspaceEventsPayloads>({
moduleName: 'workspaces'
})
export const WorkspacesEmitter = { emit, listen, events: WorkspaceEvents }
@@ -2,7 +2,7 @@ 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/dbSelector'
import { getDb } from '@/modules/multiregion/utils/dbSelector'
import { getRegionsFactory } from '@/modules/multiregion/repositories'
import { authorizeResolver } from '@/modules/shared'
import {
@@ -1,5 +1,9 @@
import { db } from '@/db/knex'
import { Resolvers } from '@/modules/core/graph/generated/graphql'
import {
Resolvers,
WorkspacePlans,
WorkspacePlanStatuses
} from '@/modules/core/graph/generated/graphql'
import { removePrivateFields } from '@/modules/core/helpers/userHelper'
import {
getProjectCollaboratorsFactory,
@@ -50,8 +54,7 @@ import {
WorkspaceJoinNotAllowedError,
WorkspaceNotFoundError,
WorkspacePaidPlanActiveError,
WorkspacesNotAuthorizedError,
WorkspacesNotYetImplementedError
WorkspacesNotAuthorizedError
} from '@/modules/workspaces/errors/workspace'
import {
deleteWorkspaceFactory as repoDeleteWorkspaceFactory,
@@ -73,7 +76,9 @@ import {
getWorkspaceBySlugFactory,
countDomainsByWorkspaceIdFactory,
getWorkspaceCreationStateFactory,
upsertWorkspaceCreationStateFactory
upsertWorkspaceCreationStateFactory,
queryWorkspacesFactory,
countWorkspacesFactory
} from '@/modules/workspaces/repositories/workspaces'
import {
buildWorkspaceInviteEmailContentsFactory,
@@ -105,7 +110,6 @@ import {
} from '@/modules/workspaces/services/projects'
import {
getDiscoverableWorkspacesForUserFactory,
getPaginatedWorkspaceTeamFactory,
getWorkspacesForUserFactory
} from '@/modules/workspaces/services/retrieval'
import {
@@ -163,7 +167,7 @@ import {
isRateLimitBreached
} from '@/modules/core/services/ratelimiter'
import { RateLimitError } from '@/modules/core/errors/ratelimit'
import { getRegionDb } from '@/modules/multiregion/dbSelector'
import { getRegionDb } from '@/modules/multiregion/utils/dbSelector'
import {
listUserExpiredSsoSessionsFactory,
listWorkspaceSsoMembershipsByUserEmailFactory
@@ -189,8 +193,15 @@ import {
} from '@/modules/automate/services/authCode'
import { getGenericRedis } from '@/modules/shared/redis/redis'
import { convertFunctionToGraphQLReturn } from '@/modules/automate/services/functionManagement'
import { getWorkspacePlanFactory } from '@/modules/gatekeeper/repositories/billing'
import {
getWorkspacePlanFactory,
upsertPaidWorkspacePlanFactory,
upsertTrialWorkspacePlanFactory,
upsertUnpaidWorkspacePlanFactory
} from '@/modules/gatekeeper/repositories/billing'
import { Knex } from 'knex'
import { getPaginatedItemsFactory } from '@/modules/shared/services/paginatedItems'
import { InvalidWorkspacePlanStatus } from '@/modules/gatekeeper/errors/billing'
const eventBus = getEventBus()
const getServerInfo = getServerInfoFactory({ db })
@@ -357,7 +368,8 @@ export = FF_WORKSPACES_MODULE_ENABLED
}
},
Mutation: {
workspaceMutations: () => ({})
workspaceMutations: () => ({}),
admin: () => ({})
},
ProjectInviteMutations: {
async createForWorkspace(_parent, args, ctx) {
@@ -414,6 +426,73 @@ export = FF_WORKSPACES_MODULE_ENABLED
return ctx.loaders.streams.getStream.load(args.projectId)
}
},
AdminMutations: {
updateWorkspacePlan: async (_parent, { input }) => {
const { workspaceId, plan: name, status } = input
const workspace = await getWorkspaceFactory({ db })({
workspaceId
})
const createdAt = new Date()
if (!workspace) throw new WorkspaceNotFoundError()
switch (name) {
case WorkspacePlans.Starter:
switch (status) {
case WorkspacePlanStatuses.Trial:
case WorkspacePlanStatuses.Expired:
await upsertTrialWorkspacePlanFactory({ db })({
workspacePlan: { workspaceId, status, name, createdAt }
})
return true
case WorkspacePlanStatuses.Valid:
case WorkspacePlanStatuses.CancelationScheduled:
case WorkspacePlanStatuses.Canceled:
case WorkspacePlanStatuses.PaymentFailed:
await upsertPaidWorkspacePlanFactory({ db })({
workspacePlan: { workspaceId, status, name, createdAt }
})
return true
default:
throwUncoveredError(status)
}
case WorkspacePlans.Business:
case WorkspacePlans.Plus:
switch (status) {
case WorkspacePlanStatuses.Trial:
case WorkspacePlanStatuses.Expired:
throw new InvalidWorkspacePlanStatus()
case WorkspacePlanStatuses.Valid:
case WorkspacePlanStatuses.CancelationScheduled:
case WorkspacePlanStatuses.Canceled:
case WorkspacePlanStatuses.PaymentFailed:
await upsertPaidWorkspacePlanFactory({ db })({
workspacePlan: { workspaceId, status, name, createdAt }
})
return true
default:
throwUncoveredError(status)
}
case WorkspacePlans.Academia:
case WorkspacePlans.Unlimited:
switch (status) {
case WorkspacePlanStatuses.Valid:
await upsertUnpaidWorkspacePlanFactory({ db })({
workspacePlan: { workspaceId, status, name, createdAt }
})
case WorkspacePlanStatuses.CancelationScheduled:
case WorkspacePlanStatuses.Canceled:
case WorkspacePlanStatuses.Expired:
case WorkspacePlanStatuses.PaymentFailed:
case WorkspacePlanStatuses.Trial:
throw new InvalidWorkspacePlanStatus()
default:
throwUncoveredError(status)
}
default:
throwUncoveredError(name)
}
}
},
WorkspaceMutations: {
create: async (_parent, args, context) => {
const { name, description, defaultLogoIndex, logo, slug } = args.input
@@ -952,10 +1031,9 @@ export = FF_WORKSPACES_MODULE_ENABLED
return workspace?.role || null
},
team: async (parent, args) => {
const team = await getPaginatedWorkspaceTeamFactory({
getWorkspaceCollaborators: getWorkspaceCollaboratorsFactory({ db }),
getWorkspaceCollaboratorsTotalCount:
getWorkspaceCollaboratorsTotalCountFactory({ db })
const team = await getPaginatedItemsFactory({
getItems: getWorkspaceCollaboratorsFactory({ db }),
getTotalCount: getWorkspaceCollaboratorsTotalCountFactory({ db })
})({
workspaceId: parent.id,
filter: removeNullOrUndefinedKeys(args?.filter || {}),
@@ -1241,8 +1319,15 @@ export = FF_WORKSPACES_MODULE_ENABLED
}
},
AdminQueries: {
workspaceList: async () => {
throw new WorkspacesNotYetImplementedError()
workspaceList: async (_parent, args) => {
return await getPaginatedItemsFactory({
getItems: queryWorkspacesFactory({ db }),
getTotalCount: countWorkspacesFactory({ db })
})({
limit: args.limit,
cursor: args.cursor,
filter: { search: args.query ?? undefined }
})
}
},
LimitedUser: {
@@ -7,6 +7,7 @@ import {
import {
CountDomainsByWorkspaceId,
CountWorkspaceRoleWithOptionalProjectRole,
CountWorkspaces,
DeleteWorkspace,
DeleteWorkspaceDomain,
DeleteWorkspaceRole,
@@ -24,6 +25,7 @@ import {
GetWorkspaceRolesForUser,
GetWorkspaceWithDomains,
GetWorkspaces,
QueryWorkspaces,
StoreWorkspaceDomain,
UpsertWorkspace,
UpsertWorkspaceCreationState,
@@ -174,6 +176,43 @@ export const getWorkspaceBySlugFactory =
return workspace || null
}
const buildWorkspacesQuery = ({ db, search }: { db: Knex; search?: string }) => {
const query = tables.workspaces(db)
if (search) {
query.andWhere((builder) => {
builder
.where('name', 'ILIKE', `%${search}%`)
.orWhere('slug', 'ILIKE', `%${search}%`)
})
}
return query
}
export const queryWorkspacesFactory =
({ db }: { db: Knex }): QueryWorkspaces =>
async ({ limit, cursor, filter }) => {
const query = buildWorkspacesQuery({ db, search: filter?.search })
.select()
.orderBy('createdAt', 'desc')
.limit(limit)
if (cursor) {
query.andWhere('createdAt', '<', cursor)
}
return await query
}
export const countWorkspacesFactory =
({ db }: { db: Knex }): CountWorkspaces =>
async ({ filter }) => {
const query = buildWorkspacesQuery({ db, search: filter?.search })
const [res] = await query.count()
const count = parseInt(res.count.toString())
return count
}
export const upsertWorkspaceFactory =
({ db }: { db: Knex }): UpsertWorkspace =>
async ({ workspace }) => {
@@ -34,7 +34,7 @@ import {
} from '@/modules/core/domain/streams/operations'
import { ProjectNotFoundError } from '@/modules/core/errors/projects'
import { WorkspaceProjectCreateInput } from '@/test/graphql/generated/graphql'
import { getDb } from '@/modules/multiregion/dbSelector'
import { getDb } from '@/modules/multiregion/utils/dbSelector'
import { createNewProjectFactory } from '@/modules/core/services/projects'
import {
deleteProjectFactory,
@@ -1,17 +1,9 @@
import { FindEmailsByUserId } from '@/modules/core/domain/userEmails/operations'
import {
decodeIsoDateCursor,
encodeIsoDateCursor
} from '@/modules/shared/helpers/graphqlHelper'
import {
GetUserDiscoverableWorkspaces,
GetWorkspace,
GetWorkspaceCollaborators,
GetWorkspaceCollaboratorsArgs,
GetWorkspaceCollaboratorsTotalCount,
GetWorkspaceRolesForUser
} from '@/modules/workspaces/domain/operations'
import { WorkspaceTeam } from '@/modules/workspaces/domain/types'
import { Workspace } from '@/modules/workspacesCore/domain/types'
import { chunk, isNull } from 'lodash'
@@ -78,38 +70,3 @@ export const getWorkspacesForUserFactory =
return workspaces
}
type WorkspaceTeamCollection = {
items: WorkspaceTeam
cursor: string | null
totalCount: number
}
export const getPaginatedWorkspaceTeamFactory =
({
getWorkspaceCollaborators,
getWorkspaceCollaboratorsTotalCount
}: {
getWorkspaceCollaborators: GetWorkspaceCollaborators
getWorkspaceCollaboratorsTotalCount: GetWorkspaceCollaboratorsTotalCount
}) =>
async (args: GetWorkspaceCollaboratorsArgs): Promise<WorkspaceTeamCollection> => {
const maybeDecodedCursor = args.cursor ? decodeIsoDateCursor(args.cursor) : null
const items = await getWorkspaceCollaborators({
...args,
cursor: maybeDecodedCursor ?? undefined
})
const totalCount = await getWorkspaceCollaboratorsTotalCount(args)
let cursor = null
if (items.length === args.limit) {
const lastItem = items.at(-1)
cursor = lastItem ? encodeIsoDateCursor(lastItem.createdAt) : null
}
return {
items,
cursor,
totalCount
}
}
@@ -73,7 +73,7 @@ import {
getDefaultRegionFactory,
upsertRegionAssignmentFactory
} from '@/modules/workspaces/repositories/regions'
import { getDb } from '@/modules/multiregion/dbSelector'
import { getDb } from '@/modules/multiregion/utils/dbSelector'
import { WorkspacePlan } from '@/modules/gatekeeper/domain/billing'
const { FF_WORKSPACES_MODULE_ENABLED } = getFeatureFlags()