From 8aadfc9be9a6a857e86d1b13b98113d4eee43ddb Mon Sep 17 00:00:00 2001 From: Alexandru Popovici Date: Tue, 14 Jan 2025 12:52:26 +0200 Subject: [PATCH 1/5] fix(viewer-lib): Fixed an issue where mismatched vertex color counts were still attempted to batch together resulting in errors. (#3809) Now if the number of vertex colors does not match the number of vertices we just ignore colors alltogether --- packages/viewer-sandbox/src/main.ts | 4 +++- .../src/modules/loaders/Speckle/SpeckleGeometryConverter.ts | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/viewer-sandbox/src/main.ts b/packages/viewer-sandbox/src/main.ts index a936e51d9..164e09f3e 100644 --- a/packages/viewer-sandbox/src/main.ts +++ b/packages/viewer-sandbox/src/main.ts @@ -459,12 +459,14 @@ const getStream = () => { // 'https://speckle.xyz/streams/27e89d0ad6/commits/5ed4b74252' //Gingerbread - 'https://latest.speckle.systems/projects/387050bffe/models/48f7eb26fb' + // 'https://latest.speckle.systems/projects/387050bffe/models/48f7eb26fb' // DUI3 Mesh Colors // 'https://app.speckle.systems/projects/93200a735d/models/cbacd3eaeb@344a397239' // Instance toilets // 'https://app.speckle.systems/projects/e89b61b65c/models/2a0995f124' + + 'https://latest.speckle.systems/projects/3fe1880c36/models/65bb4287a8' ) } diff --git a/packages/viewer/src/modules/loaders/Speckle/SpeckleGeometryConverter.ts b/packages/viewer/src/modules/loaders/Speckle/SpeckleGeometryConverter.ts index 64db0e596..866f32ffd 100644 --- a/packages/viewer/src/modules/loaders/Speckle/SpeckleGeometryConverter.ts +++ b/packages/viewer/src/modules/loaders/Speckle/SpeckleGeometryConverter.ts @@ -286,9 +286,9 @@ export class SpeckleGeometryConverter extends GeometryConverter { Logger.warn( `Mesh (id ${node.raw.id}) colours are mismatched with vertice counts. The number of colours must equal the number of vertices.` ) - } + } else /** We want the colors in linear space */ - colors = this.unpackColors(colorsRaw, true) + colors = this.unpackColors(colorsRaw, true) } return { From 37ede3b1b2749aabcd2387e194498236214b0379 Mon Sep 17 00:00:00 2001 From: Alessandro Magionami Date: Tue, 14 Jan 2025 12:49:21 +0100 Subject: [PATCH 2/5] Alessandro/web 2360 request to join workspace (#3799) * feat(workspaces): request to join workspace mutation * feat(workspaces): random email in test * feat(workspaces): update email * feat(workspaces): code review changes * chore(workspaces): fix tests --- .../typedefs/workspaces.graphql | 6 + .../modules/core/graph/generated/graphql.ts | 14 ++ .../graph/generated/graphql.ts | 11 ++ .../modules/workspaces/domain/operations.ts | 11 +- .../workspaces/graph/resolvers/workspaces.ts | 40 ++++- .../repositories/workspaceJoinRequests.ts | 16 +- .../services/workspaceJoinRequests.ts | 154 +++++++++++++++++- .../integration/workspaceJoinRequests.spec.ts | 105 +++++++++++- .../server/test/graphql/generated/graphql.ts | 11 ++ 9 files changed, 357 insertions(+), 11 deletions(-) diff --git a/packages/server/assets/workspacesCore/typedefs/workspaces.graphql b/packages/server/assets/workspacesCore/typedefs/workspaces.graphql index 8114359be..97d360cb6 100644 --- a/packages/server/assets/workspacesCore/typedefs/workspaces.graphql +++ b/packages/server/assets/workspacesCore/typedefs/workspaces.graphql @@ -155,12 +155,18 @@ type WorkspaceMutations { Dismiss a workspace from the discoverable list, behind the scene a join request is created with the status "dismissed" """ dismiss(input: WorkspaceDismissInput!): Boolean! @hasServerRole(role: SERVER_USER) + requestToJoin(input: WorkspaceRequestToJoinInput!): Boolean! + @hasServerRole(role: SERVER_USER) } input WorkspaceDismissInput { workspaceId: ID! } +input WorkspaceRequestToJoinInput { + workspaceId: ID! +} + input WorkspaceCreationStateInput { workspaceId: ID! completed: Boolean! diff --git a/packages/server/modules/core/graph/generated/graphql.ts b/packages/server/modules/core/graph/generated/graphql.ts index 2019d21b1..fb5c4c907 100644 --- a/packages/server/modules/core/graph/generated/graphql.ts +++ b/packages/server/modules/core/graph/generated/graphql.ts @@ -4404,11 +4404,13 @@ export type WorkspaceMutations = { delete: Scalars['Boolean']['output']; deleteDomain: Workspace; deleteSsoProvider: Scalars['Boolean']['output']; + /** Dismiss a workspace from the discoverable list, behind the scene a join request is created with the status "dismissed" */ dismiss: Scalars['Boolean']['output']; invites: WorkspaceInviteMutations; join: Workspace; leave: Scalars['Boolean']['output']; projects: WorkspaceProjectMutations; + requestToJoin: Scalars['Boolean']['output']; /** Set the default region where project data will be stored. Only available to admins. */ setDefaultRegion: Workspace; update: Workspace; @@ -4457,6 +4459,11 @@ export type WorkspaceMutationsLeaveArgs = { }; +export type WorkspaceMutationsRequestToJoinArgs = { + input: WorkspaceRequestToJoinInput; +}; + + export type WorkspaceMutationsSetDefaultRegionArgs = { regionKey: Scalars['String']['input']; workspaceId: Scalars['String']['input']; @@ -4566,6 +4573,10 @@ export enum WorkspaceProjectsUpdatedMessageType { Removed = 'REMOVED' } +export type WorkspaceRequestToJoinInput = { + workspaceId: Scalars['ID']['input']; +}; + export enum WorkspaceRole { Admin = 'ADMIN', Guest = 'GUEST', @@ -5001,6 +5012,7 @@ export type ResolversTypes = { WorkspaceProjectsFilter: WorkspaceProjectsFilter; WorkspaceProjectsUpdatedMessage: ResolverTypeWrapper & { project?: Maybe }>; WorkspaceProjectsUpdatedMessageType: WorkspaceProjectsUpdatedMessageType; + WorkspaceRequestToJoinInput: WorkspaceRequestToJoinInput; WorkspaceRole: WorkspaceRole; WorkspaceRoleDeleteInput: WorkspaceRoleDeleteInput; WorkspaceRoleUpdateInput: WorkspaceRoleUpdateInput; @@ -5270,6 +5282,7 @@ export type ResolversParentTypes = { WorkspaceProjectMutations: WorkspaceProjectMutationsGraphQLReturn; WorkspaceProjectsFilter: WorkspaceProjectsFilter; WorkspaceProjectsUpdatedMessage: Omit & { project?: Maybe }; + WorkspaceRequestToJoinInput: WorkspaceRequestToJoinInput; WorkspaceRoleDeleteInput: WorkspaceRoleDeleteInput; WorkspaceRoleUpdateInput: WorkspaceRoleUpdateInput; WorkspaceSso: WorkspaceSsoGraphQLReturn; @@ -6796,6 +6809,7 @@ export type WorkspaceMutationsResolvers>; leave?: Resolver>; projects?: Resolver; + requestToJoin?: Resolver>; setDefaultRegion?: Resolver>; update?: Resolver>; updateCreationState?: Resolver>; 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 eb11728b8..804fe3d90 100644 --- a/packages/server/modules/cross-server-sync/graph/generated/graphql.ts +++ b/packages/server/modules/cross-server-sync/graph/generated/graphql.ts @@ -4385,11 +4385,13 @@ export type WorkspaceMutations = { delete: Scalars['Boolean']['output']; deleteDomain: Workspace; deleteSsoProvider: Scalars['Boolean']['output']; + /** Dismiss a workspace from the discoverable list, behind the scene a join request is created with the status "dismissed" */ dismiss: Scalars['Boolean']['output']; invites: WorkspaceInviteMutations; join: Workspace; leave: Scalars['Boolean']['output']; projects: WorkspaceProjectMutations; + requestToJoin: Scalars['Boolean']['output']; /** Set the default region where project data will be stored. Only available to admins. */ setDefaultRegion: Workspace; update: Workspace; @@ -4438,6 +4440,11 @@ export type WorkspaceMutationsLeaveArgs = { }; +export type WorkspaceMutationsRequestToJoinArgs = { + input: WorkspaceRequestToJoinInput; +}; + + export type WorkspaceMutationsSetDefaultRegionArgs = { regionKey: Scalars['String']['input']; workspaceId: Scalars['String']['input']; @@ -4547,6 +4554,10 @@ export enum WorkspaceProjectsUpdatedMessageType { Removed = 'REMOVED' } +export type WorkspaceRequestToJoinInput = { + workspaceId: Scalars['ID']['input']; +}; + export enum WorkspaceRole { Admin = 'ADMIN', Guest = 'GUEST', diff --git a/packages/server/modules/workspaces/domain/operations.ts b/packages/server/modules/workspaces/domain/operations.ts index 8ce7e2c21..f96afa841 100644 --- a/packages/server/modules/workspaces/domain/operations.ts +++ b/packages/server/modules/workspaces/domain/operations.ts @@ -315,4 +315,13 @@ export type UpdateWorkspaceJoinRequestStatus = (params: { workspaceId: string userId: string status: WorkspaceJoinRequestStatus -}) => Promise | undefined> +}) => Promise + +export type CreateWorkspaceJoinRequest = (params: { + workspaceJoinRequest: Omit +}) => Promise + +export type SendWorkspaceJoinRequestReceivedEmail = (params: { + workspace: Pick + requester: { id: string; name: string; email: string } +}) => Promise diff --git a/packages/server/modules/workspaces/graph/resolvers/workspaces.ts b/packages/server/modules/workspaces/graph/resolvers/workspaces.ts index 64825bbe4..b392a16c9 100644 --- a/packages/server/modules/workspaces/graph/resolvers/workspaces.ts +++ b/packages/server/modules/workspaces/graph/resolvers/workspaces.ts @@ -203,8 +203,15 @@ import { Knex } from 'knex' import { getPaginatedItemsFactory } from '@/modules/shared/services/paginatedItems' import { InvalidWorkspacePlanStatus } from '@/modules/gatekeeper/errors/billing' import { BadRequestError } from '@/modules/shared/errors' -import { dismissWorkspaceJoinRequestFactory } from '@/modules/workspaces/services/workspaceJoinRequests' -import { updateWorkspaceJoinRequestStatusFactory } from '@/modules/workspaces/repositories/workspaceJoinRequests' +import { + dismissWorkspaceJoinRequestFactory, + requestToJoinWorkspaceFactory, + sendWorkspaceJoinRequestReceivedEmailFactory +} from '@/modules/workspaces/services/workspaceJoinRequests' +import { + createWorkspaceJoinRequestFactory, + updateWorkspaceJoinRequestStatusFactory +} from '@/modules/workspaces/repositories/workspaceJoinRequests' const eventBus = getEventBus() const getServerInfo = getServerInfoFactory({ db }) @@ -784,6 +791,35 @@ export = FF_WORKSPACES_MODULE_ENABLED db }) })({ userId: ctx.userId!, workspaceId: args.input.workspaceId }) + }, + requestToJoin: async (_parent, args, ctx) => { + const transaction = await db.transaction() + const createWorkspaceJoinRequest = createWorkspaceJoinRequestFactory({ + db: transaction + }) + const sendWorkspaceJoinRequestReceivedEmail = + sendWorkspaceJoinRequestReceivedEmailFactory({ + renderEmail, + sendEmail, + getServerInfo, + getWorkspaceCollaborators: getWorkspaceCollaboratorsFactory({ + db: transaction + }), + getUserEmails: findEmailsByUserIdFactory({ db: transaction }) + }) + + return await withTransaction( + requestToJoinWorkspaceFactory({ + createWorkspaceJoinRequest, + sendWorkspaceJoinRequestReceivedEmail, + getUserById: getUserFactory({ db: transaction }), + getWorkspace: getWorkspaceFactory({ db: transaction }) + })({ + userId: ctx.userId!, + workspaceId: args.input.workspaceId + }), + transaction + ) } }, WorkspaceInviteMutations: { diff --git a/packages/server/modules/workspaces/repositories/workspaceJoinRequests.ts b/packages/server/modules/workspaces/repositories/workspaceJoinRequests.ts index a9ab04ca5..8c3ff44a6 100644 --- a/packages/server/modules/workspaces/repositories/workspaceJoinRequests.ts +++ b/packages/server/modules/workspaces/repositories/workspaceJoinRequests.ts @@ -1,4 +1,7 @@ -import { UpdateWorkspaceJoinRequestStatus } from '@/modules/workspaces/domain/operations' +import { + CreateWorkspaceJoinRequest, + UpdateWorkspaceJoinRequestStatus +} from '@/modules/workspaces/domain/operations' import { WorkspaceJoinRequest } from '@/modules/workspacesCore/domain/types' import { WorkspaceJoinRequests } from '@/modules/workspacesCore/helpers/db' import { Knex } from 'knex' @@ -8,14 +11,19 @@ const tables = { db(WorkspaceJoinRequests.name) } +export const createWorkspaceJoinRequestFactory = + ({ db }: { db: Knex }): CreateWorkspaceJoinRequest => + async ({ workspaceJoinRequest }) => { + const res = await tables.workspaceJoinRequests(db).insert(workspaceJoinRequest, '*') + return res[0] + } + export const updateWorkspaceJoinRequestStatusFactory = ({ db }: { db: Knex }): UpdateWorkspaceJoinRequestStatus => async ({ workspaceId, userId, status }) => { - const [request] = await tables + return await tables .workspaceJoinRequests(db) .insert({ workspaceId, userId, status }) .onConflict(['workspaceId', 'userId']) .merge(['status']) - .returning('*') - return request } diff --git a/packages/server/modules/workspaces/services/workspaceJoinRequests.ts b/packages/server/modules/workspaces/services/workspaceJoinRequests.ts index f758bc091..43b431f60 100644 --- a/packages/server/modules/workspaces/services/workspaceJoinRequests.ts +++ b/packages/server/modules/workspaces/services/workspaceJoinRequests.ts @@ -1,8 +1,18 @@ +import { WorkspaceNotFoundError } from '@/modules/workspaces/errors/workspace' +import { GetServerInfo } from '@/modules/core/domain/server/operations' +import { FindEmailsByUserId } from '@/modules/core/domain/userEmails/operations' +import { GetUser } from '@/modules/core/domain/users/operations' +import { RenderEmail, SendEmail } from '@/modules/emails/domain/operations' +import { NotFoundError } from '@/modules/shared/errors' +import { getFrontendOrigin } from '@/modules/shared/helpers/envHelper' import { + CreateWorkspaceJoinRequest, GetWorkspace, + GetWorkspaceCollaborators, + SendWorkspaceJoinRequestReceivedEmail, UpdateWorkspaceJoinRequestStatus } from '@/modules/workspaces/domain/operations' -import { WorkspaceNotFoundError } from '@/modules/workspaces/errors/workspace' +import { Roles } from '@speckle/shared' export const dismissWorkspaceJoinRequestFactory = ({ @@ -24,3 +34,145 @@ export const dismissWorkspaceJoinRequestFactory = }) return true } + +type WorkspaceJoinRequestReceivedEmailArgs = { + workspace: { id: string; name: string; slug: string } + requester: { name: string } + workspaceAdmin: { id: string; name: string } +} + +const buildMjmlBody = ({ + workspace, + requester, + workspaceAdmin +}: WorkspaceJoinRequestReceivedEmailArgs) => { + const bodyStart = ` +Hi ${workspaceAdmin.name}! +
+
+${requester.name} is requesting to join your workspace ${workspace.name}. +
+
+ +
+ ` + const bodyEnd = ` +Have questions or feedback? Please write us at hello@speckle.systems and we'd be more than happy to talk. + ` + return { bodyStart, bodyEnd } +} + +const buildTextBody = ({ + workspace, + requester, + workspaceAdmin +}: WorkspaceJoinRequestReceivedEmailArgs) => { + const bodyStart = ` +Hi ${workspaceAdmin.name}! +\r\n\r\n +${requester.name} is requesting to join your workspace ${workspace.name}. +\r\n\r\n + ` + const bodyEnd = `Have questions or feedback? Please write us at hello@speckle.systems and we'd be more than happy to talk.` + return { bodyStart, bodyEnd } +} + +const buildEmailTemplateParams = (args: WorkspaceJoinRequestReceivedEmailArgs) => { + const url = new URL( + `workspaces/${args.workspace.slug}`, + getFrontendOrigin() + ).toString() + return { + mjml: buildMjmlBody(args), + text: buildTextBody(args), + cta: { + title: 'Manage Members', + url + } + } +} + +export const sendWorkspaceJoinRequestReceivedEmailFactory = + ({ + renderEmail, + sendEmail, + getServerInfo, + getWorkspaceCollaborators, + getUserEmails + }: { + renderEmail: RenderEmail + sendEmail: SendEmail + getServerInfo: GetServerInfo + getWorkspaceCollaborators: GetWorkspaceCollaborators + getUserEmails: FindEmailsByUserId + }) => + async (args: Omit) => { + const { requester, workspace } = args + const [serverInfo, workspaceAdmins] = await Promise.all([ + getServerInfo(), + getWorkspaceCollaborators({ + workspaceId: workspace.id, + limit: 100, + filter: { roles: [Roles.Workspace.Admin] } + }) + ]) + const sendEmailParams = await Promise.all( + workspaceAdmins.map(async (admin) => { + const userEmails = await getUserEmails({ userId: admin.id }) + const emailTemplateParams = buildEmailTemplateParams({ + requester, + workspace, + workspaceAdmin: admin + }) + const { html, text } = await renderEmail(emailTemplateParams, serverInfo, null) + const subject = `${requester.name} wants to join your workspace` + const sendEmailParams = { + html, + text, + subject, + to: userEmails.map((e) => e.email) + } + return sendEmailParams + }) + ) + await Promise.all(sendEmailParams.map((params) => sendEmail(params))) + } + +export const requestToJoinWorkspaceFactory = + ({ + createWorkspaceJoinRequest, + sendWorkspaceJoinRequestReceivedEmail, + getUserById, + getWorkspace + }: { + createWorkspaceJoinRequest: CreateWorkspaceJoinRequest + sendWorkspaceJoinRequestReceivedEmail: SendWorkspaceJoinRequestReceivedEmail + getUserById: GetUser + getWorkspace: GetWorkspace + }) => + async ({ userId, workspaceId }: { userId: string; workspaceId: string }) => { + await createWorkspaceJoinRequest({ + workspaceJoinRequest: { + userId, + workspaceId, + status: 'pending' + } + }) + + const requester = await getUserById(userId) + if (!requester) { + throw new NotFoundError('User not found') + } + + const workspace = await getWorkspace({ workspaceId }) + if (!workspace) { + throw new NotFoundError('Workspace not found') + } + + await sendWorkspaceJoinRequestReceivedEmail({ + workspace, + requester + }) + + return true + } diff --git a/packages/server/modules/workspaces/tests/integration/workspaceJoinRequests.spec.ts b/packages/server/modules/workspaces/tests/integration/workspaceJoinRequests.spec.ts index 2f8b3c598..c6a717863 100644 --- a/packages/server/modules/workspaces/tests/integration/workspaceJoinRequests.spec.ts +++ b/packages/server/modules/workspaces/tests/integration/workspaceJoinRequests.spec.ts @@ -1,20 +1,35 @@ import { db } from '@/db/knex' -import { createRandomString } from '@/modules/core/helpers/testHelpers' +import { + createRandomEmail, + createRandomString +} from '@/modules/core/helpers/testHelpers' import { getFeatureFlags } from '@/modules/shared/helpers/envHelper' import { WorkspaceNotFoundError } from '@/modules/workspaces/errors/workspace' -import { updateWorkspaceJoinRequestStatusFactory } from '@/modules/workspaces/repositories/workspaceJoinRequests' import { getWorkspaceFactory } from '@/modules/workspaces/repositories/workspaces' -import { dismissWorkspaceJoinRequestFactory } from '@/modules/workspaces/services/workspaceJoinRequests' +import { UserWithOptionalRole } from '@/modules/core/repositories/users' +import { + CreateWorkspaceJoinRequest, + SendWorkspaceJoinRequestReceivedEmail +} from '@/modules/workspaces/domain/operations' +import { + dismissWorkspaceJoinRequestFactory, + requestToJoinWorkspaceFactory +} from '@/modules/workspaces/services/workspaceJoinRequests' import { BasicTestWorkspace, createTestWorkspace } from '@/modules/workspaces/tests/helpers/creation' +import { Workspace, WorkspaceJoinRequest } from '@/modules/workspacesCore/domain/types' import { WorkspaceJoinRequests } from '@/modules/workspacesCore/helpers/db' import { expectToThrow } from '@/test/assertionHelper' import { BasicTestUser, createTestUser } from '@/test/authHelper' import { Roles } from '@speckle/shared' import { expect } from 'chai' import cryptoRandomString from 'crypto-random-string' +import { + createWorkspaceJoinRequestFactory, + updateWorkspaceJoinRequestStatusFactory +} from '@/modules/workspaces/repositories/workspaceJoinRequests' const { FF_WORKSPACES_MODULE_ENABLED } = getFeatureFlags() @@ -70,5 +85,89 @@ const { FF_WORKSPACES_MODULE_ENABLED } = getFeatureFlags() ).to.deep.equal({ status: 'dismissed' }) }) }) + + describe('requestToJoinWorkspaceFactory, returns a function that ', () => { + it('throws a NotFoundError if the user does not exists', async () => { + const err = await expectToThrow(() => + requestToJoinWorkspaceFactory({ + createWorkspaceJoinRequest: (async () => + Promise.resolve()) as unknown as CreateWorkspaceJoinRequest, + sendWorkspaceJoinRequestReceivedEmail: async () => Promise.resolve(), + getUserById: async () => null, + getWorkspace: async () => null + })({ workspaceId: createRandomString(), userId: createRandomString() }) + ) + + expect(err.message).to.equal('User not found') + }) + it('throws a WorkspaceNotFoundError if the workspace does not exists', async () => { + const user = await createTestUser({}) + const err = await expectToThrow(() => + requestToJoinWorkspaceFactory({ + createWorkspaceJoinRequest: (async () => + Promise.resolve()) as unknown as CreateWorkspaceJoinRequest, + sendWorkspaceJoinRequestReceivedEmail: async () => Promise.resolve(), + getUserById: async () => user as unknown as UserWithOptionalRole, + getWorkspace: async () => null + })({ workspaceId: createRandomString(), userId: createRandomString() }) + ) + + expect(err.message).to.equal(WorkspaceNotFoundError.defaultMessage) + }) + it('creates a join request and sends an email to all admins', async () => { + const createWorkspaceJoinRequest = createWorkspaceJoinRequestFactory({ db }) + + const sendWorkspaceJoinRequestReceivedEmailCalls: Parameters[number][] = + [] + const sendWorkspaceJoinRequestReceivedEmail = async ( + args: Parameters[number] + ) => sendWorkspaceJoinRequestReceivedEmailCalls.push(args) + + const user: BasicTestUser = { + id: '', + name: 'John Speckle', + email: createRandomEmail(), + role: Roles.Server.Admin, + verified: true + } + + await createTestUser(user) + + const workspace: BasicTestWorkspace = { + id: '', + slug: '', + ownerId: '', + name: cryptoRandomString({ length: 6 }), + description: cryptoRandomString({ length: 12 }) + } + await createTestWorkspace(workspace, user) + + expect( + await requestToJoinWorkspaceFactory({ + createWorkspaceJoinRequest, + sendWorkspaceJoinRequestReceivedEmail: + sendWorkspaceJoinRequestReceivedEmail as unknown as SendWorkspaceJoinRequestReceivedEmail, + getUserById: async () => user as unknown as UserWithOptionalRole, + getWorkspace: async () => workspace as unknown as Workspace + })({ workspaceId: workspace.id, userId: user.id }) + ).to.equal(true) + + expect( + (await db(WorkspaceJoinRequests.name) + .where({ + workspaceId: workspace.id, + userId: user.id + }) + .select('status') + .first())!.status + ).to.equal('pending') + + expect(sendWorkspaceJoinRequestReceivedEmailCalls).to.have.length(1) + expect(sendWorkspaceJoinRequestReceivedEmailCalls[0].workspace).to.equal( + workspace + ) + expect(sendWorkspaceJoinRequestReceivedEmailCalls[0].requester).to.equal(user) + }) + }) } ) diff --git a/packages/server/test/graphql/generated/graphql.ts b/packages/server/test/graphql/generated/graphql.ts index 1e328b432..c31fe7bdf 100644 --- a/packages/server/test/graphql/generated/graphql.ts +++ b/packages/server/test/graphql/generated/graphql.ts @@ -4386,11 +4386,13 @@ export type WorkspaceMutations = { delete: Scalars['Boolean']['output']; deleteDomain: Workspace; deleteSsoProvider: Scalars['Boolean']['output']; + /** Dismiss a workspace from the discoverable list, behind the scene a join request is created with the status "dismissed" */ dismiss: Scalars['Boolean']['output']; invites: WorkspaceInviteMutations; join: Workspace; leave: Scalars['Boolean']['output']; projects: WorkspaceProjectMutations; + requestToJoin: Scalars['Boolean']['output']; /** Set the default region where project data will be stored. Only available to admins. */ setDefaultRegion: Workspace; update: Workspace; @@ -4439,6 +4441,11 @@ export type WorkspaceMutationsLeaveArgs = { }; +export type WorkspaceMutationsRequestToJoinArgs = { + input: WorkspaceRequestToJoinInput; +}; + + export type WorkspaceMutationsSetDefaultRegionArgs = { regionKey: Scalars['String']['input']; workspaceId: Scalars['String']['input']; @@ -4548,6 +4555,10 @@ export enum WorkspaceProjectsUpdatedMessageType { Removed = 'REMOVED' } +export type WorkspaceRequestToJoinInput = { + workspaceId: Scalars['ID']['input']; +}; + export enum WorkspaceRole { Admin = 'ADMIN', Guest = 'GUEST', From 9636a56b001b1b39d7d62214f9dd1025f34afac7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gerg=C5=91=20Jedlicska?= <57442769+gjedlicska@users.noreply.github.com> Date: Tue, 14 Jan 2025 13:02:09 +0100 Subject: [PATCH 3/5] feat(server): server info lookup cache (#3808) --- .../modules/core/graph/resolvers/server.ts | 18 ++++++++++++++++-- .../server/modules/core/repositories/server.ts | 16 ++++++++++++++++ 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/packages/server/modules/core/graph/resolvers/server.ts b/packages/server/modules/core/graph/resolvers/server.ts index b8cafa420..7b80e443c 100644 --- a/packages/server/modules/core/graph/resolvers/server.ts +++ b/packages/server/modules/core/graph/resolvers/server.ts @@ -9,11 +9,18 @@ import { getServerInfoFactory, updateServerInfoFactory, getPublicRolesFactory, - getPublicScopesFactory + getPublicScopesFactory, + getServerInfoFromCacheFactory, + storeServerInfoInCacheFactory } from '@/modules/core/repositories/server' import { db } from '@/db/knex' import { Resolvers } from '@/modules/core/graph/generated/graphql' +import { LRUCache } from 'lru-cache' +import { ServerInfo } from '@/modules/core/helpers/types' +const cache = new LRUCache({ max: 1, ttl: 60 * 1000 }) +const getServerInfoFromCache = getServerInfoFromCacheFactory({ cache }) +const storeServerInfoInCache = storeServerInfoInCacheFactory({ cache }) const getServerInfo = getServerInfoFactory({ db }) const updateServerInfo = updateServerInfoFactory({ db }) const getPublicRoles = getPublicRolesFactory({ db }) @@ -22,7 +29,11 @@ const getPublicScopes = getPublicScopesFactory({ db }) export = { Query: { async serverInfo() { - return await getServerInfo() + const cachedServerInfo = getServerInfoFromCache() + if (cachedServerInfo) return cachedServerInfo + const serverInfo = await getServerInfo() + storeServerInfoInCache({ serverInfo }) + return serverInfo } }, ServerInfo: { @@ -58,6 +69,9 @@ export = { const update = removeNullOrUndefinedKeys(args.info) await updateServerInfo(update) + // we're currently going to ignore, that this should be propagated to all + // backend instances, and going to rely on the TTL in the cache to propagate the changes + cache.clear() return true }, serverInfoMutations: () => ({}) diff --git a/packages/server/modules/core/repositories/server.ts b/packages/server/modules/core/repositories/server.ts index c24477755..1360adba9 100644 --- a/packages/server/modules/core/repositories/server.ts +++ b/packages/server/modules/core/repositories/server.ts @@ -18,6 +18,7 @@ import { getServerVersion } from '@/modules/shared/helpers/envHelper' import { Knex } from 'knex' +import { LRUCache } from 'lru-cache' const ServerConfig = buildTableHelper('server_config', [ 'id', @@ -38,6 +39,21 @@ const tables = { scopes: (db: Knex) => db(Scopes.name) } +const SERVER_CONFIG_CACHE_KEY = 'server_config' + +export const getServerInfoFromCacheFactory = + ({ cache }: { cache: LRUCache }) => + () => { + const serverInfo = cache.get(SERVER_CONFIG_CACHE_KEY) + return serverInfo ?? null + } + +export const storeServerInfoInCacheFactory = + ({ cache }: { cache: LRUCache }) => + ({ serverInfo }: { serverInfo: ServerInfo }) => { + cache.set(SERVER_CONFIG_CACHE_KEY, serverInfo) + } + export const getServerInfoFactory = (deps: { db: Knex }): GetServerInfo => async () => { From 25390250df9bb4a6274355c68f31623b8dcb828a Mon Sep 17 00:00:00 2001 From: andrewwallacespeckle <139135120+andrewwallacespeckle@users.noreply.github.com> Date: Tue, 14 Jan 2025 13:08:33 +0000 Subject: [PATCH 4/5] refactor(fe2): Further gendo polishing (#3814) * Add ... to all suggested prompts * Update copy and remove some icons * Update spacings * Increase gaps * Some more polishing from call * Add submit on enter * Fix overflow * Typo * keyup > keypress --------- Co-authored-by: Benjamin Ottensten --- .../components/viewer/gendo/Item.vue | 6 +- .../components/viewer/gendo/List.vue | 2 +- .../components/viewer/gendo/Panel.vue | 70 +++++++------------ .../lib/common/generated/gql/graphql.ts | 12 ++++ 4 files changed, 41 insertions(+), 49 deletions(-) diff --git a/packages/frontend-2/components/viewer/gendo/Item.vue b/packages/frontend-2/components/viewer/gendo/Item.vue index 95da614e9..4adc50404 100644 --- a/packages/frontend-2/components/viewer/gendo/Item.vue +++ b/packages/frontend-2/components/viewer/gendo/Item.vue @@ -1,5 +1,5 @@ -
-
- - +
+
+ + -
+
- Writing prompts + Learn to prompt
- - Generate - + + + Generate + +
- - Feedback + Give us feedback
Terms -
-