feat(workspaces): project creation emit domain event

* feat(workspaces): drop createdByUserId from the dataschema

* feat(workspaces): repositories WIP

* merge

* protect against removing last admin in workspace

* quick impl and stub tests

* add tests

* services

* unit tests for role services

* feat(workspaces): authorize project creation if workspace specified

* feat(workspaces): emit project created event

* fix(workspaces): protect against adding a project to a workspace if module not enabled

* fix(workspaces): oops broke tests during merge

---------

Co-authored-by: Gergő Jedlicska <gergo@jedlicska.com>
This commit is contained in:
Chuck Driesler
2024-07-10 16:13:41 +01:00
committed by GitHub
parent ef50b2c298
commit 790d97383c
12 changed files with 75 additions and 2 deletions
@@ -30,6 +30,7 @@ input ProjectCreateInput {
name: String
description: String
visibility: ProjectVisibility
workspaceId: String
}
input ProjectUpdateRoleInput {
@@ -316,6 +316,7 @@ input StreamCreateInput {
Optionally specify user IDs of users that you want to invite to be contributors to this stream
"""
withContributors: [String!]
workspaceId: String
}
input StreamUpdateInput {
@@ -0,0 +1,7 @@
import { BaseError } from '@/modules/shared/errors/base'
export class WorkspacesModuleDisabledError extends BaseError {
static defaultMessage = 'Workspaces are not enabled on this server'
static code = 'WORKSPACES_MODULE_DISABLED_ERROR'
static statusCode = 403
}
@@ -0,0 +1,19 @@
import { StreamRecord } from '@/modules/core/helpers/types'
import { initializeModuleEventEmitter } from '@/modules/shared/services/moduleEventEmitterSetup'
export const ProjectEvents = {
Created: 'created'
} as const
export type ProjectEvents = (typeof ProjectEvents)[keyof typeof ProjectEvents]
export type ProjectEventsPayloads = {
[ProjectEvents.Created]: { project: StreamRecord }
}
const { emit, listen } = initializeModuleEventEmitter<ProjectEventsPayloads>({
moduleName: 'core',
namespace: 'projects'
})
export const ProjectsEmitter = { emit, listen, events: ProjectEvents }
@@ -2015,6 +2015,7 @@ export type ProjectCreateInput = {
description?: InputMaybe<Scalars['String']['input']>;
name?: InputMaybe<Scalars['String']['input']>;
visibility?: InputMaybe<ProjectVisibility>;
workspaceId?: InputMaybe<Scalars['String']['input']>;
};
export type ProjectFileImportUpdatedMessage = {
@@ -2886,6 +2887,7 @@ export type StreamCreateInput = {
name?: InputMaybe<Scalars['String']['input']>;
/** Optionally specify user IDs of users that you want to invite to be contributors to this stream */
withContributors?: InputMaybe<Array<Scalars['String']['input']>>;
workspaceId?: InputMaybe<Scalars['String']['input']>;
};
export type StreamInviteCreateInput = {
@@ -1,11 +1,13 @@
import db from '@/db/knex'
import { RateLimitError } from '@/modules/core/errors/ratelimit'
import { StreamNotFoundError } from '@/modules/core/errors/stream'
import { WorkspacesModuleDisabledError } from '@/modules/core/errors/workspaces'
import {
ProjectVisibility,
Resolvers,
TokenResourceIdentifierType
} from '@/modules/core/graph/generated/graphql'
import { isWorkspacesModuleEnabled } from '@/modules/core/helpers/features'
import { Roles, Scopes, StreamRoles } from '@/modules/core/helpers/mainConstants'
import { isResourceAllowed, toProjectIdWhitelist } from '@/modules/core/helpers/token'
import {
@@ -114,6 +116,19 @@ export = {
throw new RateLimitError(rateLimitResult)
}
if (!!args.input?.workspaceId) {
if (!isWorkspacesModuleEnabled()) {
// Ugly but complete, will go away if/when resolver moved to workspaces module
throw new WorkspacesModuleDisabledError()
}
await authorizeResolver(
context.userId!,
args.input.workspaceId,
Roles.Workspace.Member,
context.resourceAccessRules
)
}
const project = await createStreamReturnRecord(
{
...(args.input || {}),
@@ -61,6 +61,8 @@ const {
queryAllStreamInvitesFactory
} = require('@/modules/serverinvites/repositories/serverInvites')
const db = require('@/db/knex')
const { isWorkspacesModuleEnabled } = require('@/modules/core/helpers/features')
const { WorkspacesModuleDisabledError } = require('@/modules/core/errors/workspaces')
// subscription events
const USER_STREAM_ADDED = StreamPubsubEvents.UserStreamAdded
@@ -259,6 +261,19 @@ module.exports = {
throw new RateLimitError(rateLimitResult)
}
if (args.stream.workspaceId) {
if (!isWorkspacesModuleEnabled()) {
// Ugly but complete, will go away if/when resolver moved to workspaces module
throw new WorkspacesModuleDisabledError()
}
await authorizeResolver(
context.userId,
args.stream.workspaceId,
Roles.Workspace.Member,
context.resourceAccessRules
)
}
const { id } = await createStreamReturnRecord(
{
...args.stream,
@@ -0,0 +1,5 @@
import { getFeatureFlags } from '@/modules/shared/helpers/envHelper'
export const isWorkspacesModuleEnabled = (): boolean => {
return getFeatureFlags().FF_WORKSPACES_MODULE_ENABLED
}
@@ -761,7 +761,7 @@ export async function createStream(
trx: Knex.Transaction
}>
) {
const { name, description } = input
const { name, description, workspaceId } = input
const { ownerId, trx } = options || {}
let shouldBePublic: boolean, shouldBeDiscoverable: boolean
@@ -782,7 +782,8 @@ export async function createStream(
description: description || '',
isPublic: shouldBePublic,
isDiscoverable: shouldBeDiscoverable,
updatedAt: knex.fn.now()
updatedAt: knex.fn.now(),
workspaceId: workspaceId || null
}
// Create the stream & set up permissions
@@ -52,6 +52,7 @@ import {
TokenResourceIdentifier,
TokenResourceIdentifierType
} from '@/modules/core/domain/tokens/types'
import { ProjectEvents, ProjectsEmitter } from '@/modules/core/events/projectsEmitter'
export async function createStreamReturnRecord(
params: (StreamCreateInput | ProjectCreateInput) & {
@@ -106,6 +107,8 @@ export async function createStreamReturnRecord(
})
}
await ProjectsEmitter.emit(ProjectEvents.Created, { project: stream })
return stream
}
@@ -2005,6 +2005,7 @@ export type ProjectCreateInput = {
description?: InputMaybe<Scalars['String']['input']>;
name?: InputMaybe<Scalars['String']['input']>;
visibility?: InputMaybe<ProjectVisibility>;
workspaceId?: InputMaybe<Scalars['String']['input']>;
};
export type ProjectFileImportUpdatedMessage = {
@@ -2876,6 +2877,7 @@ export type StreamCreateInput = {
name?: InputMaybe<Scalars['String']['input']>;
/** Optionally specify user IDs of users that you want to invite to be contributors to this stream */
withContributors?: InputMaybe<Array<Scalars['String']['input']>>;
workspaceId?: InputMaybe<Scalars['String']['input']>;
};
export type StreamInviteCreateInput = {
@@ -2006,6 +2006,7 @@ export type ProjectCreateInput = {
description?: InputMaybe<Scalars['String']['input']>;
name?: InputMaybe<Scalars['String']['input']>;
visibility?: InputMaybe<ProjectVisibility>;
workspaceId?: InputMaybe<Scalars['String']['input']>;
};
export type ProjectFileImportUpdatedMessage = {
@@ -2877,6 +2878,7 @@ export type StreamCreateInput = {
name?: InputMaybe<Scalars['String']['input']>;
/** Optionally specify user IDs of users that you want to invite to be contributors to this stream */
withContributors?: InputMaybe<Array<Scalars['String']['input']>>;
workspaceId?: InputMaybe<Scalars['String']['input']>;
};
export type StreamInviteCreateInput = {