diff --git a/packages/server/assets/core/typedefs/projects.graphql b/packages/server/assets/core/typedefs/projects.graphql index 77c1e70fe..548010d9b 100644 --- a/packages/server/assets/core/typedefs/projects.graphql +++ b/packages/server/assets/core/typedefs/projects.graphql @@ -30,6 +30,7 @@ input ProjectCreateInput { name: String description: String visibility: ProjectVisibility + workspaceId: String } input ProjectUpdateRoleInput { diff --git a/packages/server/assets/core/typedefs/streams.graphql b/packages/server/assets/core/typedefs/streams.graphql index 57f425cfd..ac07a9236 100644 --- a/packages/server/assets/core/typedefs/streams.graphql +++ b/packages/server/assets/core/typedefs/streams.graphql @@ -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 { diff --git a/packages/server/modules/core/errors/workspaces.ts b/packages/server/modules/core/errors/workspaces.ts new file mode 100644 index 000000000..cf9b59962 --- /dev/null +++ b/packages/server/modules/core/errors/workspaces.ts @@ -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 +} diff --git a/packages/server/modules/core/events/projectsEmitter.ts b/packages/server/modules/core/events/projectsEmitter.ts new file mode 100644 index 000000000..dd55491ef --- /dev/null +++ b/packages/server/modules/core/events/projectsEmitter.ts @@ -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({ + moduleName: 'core', + namespace: 'projects' +}) + +export const ProjectsEmitter = { emit, listen, events: ProjectEvents } diff --git a/packages/server/modules/core/graph/generated/graphql.ts b/packages/server/modules/core/graph/generated/graphql.ts index 2f289df5b..87e4e1b39 100644 --- a/packages/server/modules/core/graph/generated/graphql.ts +++ b/packages/server/modules/core/graph/generated/graphql.ts @@ -2015,6 +2015,7 @@ export type ProjectCreateInput = { description?: InputMaybe; name?: InputMaybe; visibility?: InputMaybe; + workspaceId?: InputMaybe; }; export type ProjectFileImportUpdatedMessage = { @@ -2886,6 +2887,7 @@ export type StreamCreateInput = { name?: InputMaybe; /** Optionally specify user IDs of users that you want to invite to be contributors to this stream */ withContributors?: InputMaybe>; + workspaceId?: InputMaybe; }; export type StreamInviteCreateInput = { diff --git a/packages/server/modules/core/graph/resolvers/projects.ts b/packages/server/modules/core/graph/resolvers/projects.ts index 37f65ea62..a3e595370 100644 --- a/packages/server/modules/core/graph/resolvers/projects.ts +++ b/packages/server/modules/core/graph/resolvers/projects.ts @@ -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 || {}), diff --git a/packages/server/modules/core/graph/resolvers/streams.js b/packages/server/modules/core/graph/resolvers/streams.js index 52d618eb7..79c2b6ba3 100644 --- a/packages/server/modules/core/graph/resolvers/streams.js +++ b/packages/server/modules/core/graph/resolvers/streams.js @@ -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, diff --git a/packages/server/modules/core/helpers/features.ts b/packages/server/modules/core/helpers/features.ts new file mode 100644 index 000000000..9908ee0f1 --- /dev/null +++ b/packages/server/modules/core/helpers/features.ts @@ -0,0 +1,5 @@ +import { getFeatureFlags } from '@/modules/shared/helpers/envHelper' + +export const isWorkspacesModuleEnabled = (): boolean => { + return getFeatureFlags().FF_WORKSPACES_MODULE_ENABLED +} diff --git a/packages/server/modules/core/repositories/streams.ts b/packages/server/modules/core/repositories/streams.ts index 8eecdca12..c89b9d6c2 100644 --- a/packages/server/modules/core/repositories/streams.ts +++ b/packages/server/modules/core/repositories/streams.ts @@ -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 diff --git a/packages/server/modules/core/services/streams/management.ts b/packages/server/modules/core/services/streams/management.ts index 83ca399d7..165843a83 100644 --- a/packages/server/modules/core/services/streams/management.ts +++ b/packages/server/modules/core/services/streams/management.ts @@ -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 } diff --git a/packages/server/modules/cross-server-sync/graph/generated/graphql.ts b/packages/server/modules/cross-server-sync/graph/generated/graphql.ts index f5be26a29..5374ebb9e 100644 --- a/packages/server/modules/cross-server-sync/graph/generated/graphql.ts +++ b/packages/server/modules/cross-server-sync/graph/generated/graphql.ts @@ -2005,6 +2005,7 @@ export type ProjectCreateInput = { description?: InputMaybe; name?: InputMaybe; visibility?: InputMaybe; + workspaceId?: InputMaybe; }; export type ProjectFileImportUpdatedMessage = { @@ -2876,6 +2877,7 @@ export type StreamCreateInput = { name?: InputMaybe; /** Optionally specify user IDs of users that you want to invite to be contributors to this stream */ withContributors?: InputMaybe>; + workspaceId?: InputMaybe; }; export type StreamInviteCreateInput = { diff --git a/packages/server/test/graphql/generated/graphql.ts b/packages/server/test/graphql/generated/graphql.ts index 8aa3a9bdc..78fff5bbe 100644 --- a/packages/server/test/graphql/generated/graphql.ts +++ b/packages/server/test/graphql/generated/graphql.ts @@ -2006,6 +2006,7 @@ export type ProjectCreateInput = { description?: InputMaybe; name?: InputMaybe; visibility?: InputMaybe; + workspaceId?: InputMaybe; }; export type ProjectFileImportUpdatedMessage = { @@ -2877,6 +2878,7 @@ export type StreamCreateInput = { name?: InputMaybe; /** Optionally specify user IDs of users that you want to invite to be contributors to this stream */ withContributors?: InputMaybe>; + workspaceId?: InputMaybe; }; export type StreamInviteCreateInput = {