diff --git a/.circleci/config.yml b/.circleci/config.yml index 68613573c..47eed7ce6 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -527,10 +527,6 @@ jobs: - run: command: cp .env.test-example .env.test working_directory: 'packages/server' - - run: - name: 'Lint' - command: yarn lint:ci - working_directory: 'packages/server' - run: name: 'Run tests' # Extra formatting to get timestamps on each line in CI (for profiling purposes) @@ -546,18 +542,6 @@ jobs: no_output_timeout: 30m - codecov/upload: files: packages/server/coverage/lcov.info - - run: - name: Introspect GQL schema for subsequent checks - command: 'IGNORE_MISSING_MIGRATIONS=true yarn cli graphql introspect' - working_directory: 'packages/server' - - run: - name: Checking for GQL schema breakages against app.speckle.systems - command: 'yarn rover graph check Speckle-Server@app-speckle-systems --schema ./introspected-schema.graphql' - working_directory: 'packages/server' - - run: - name: Checking for GQL schema breakages against latest.speckle.systems - command: 'yarn rover graph check Speckle-Server@latest-speckle-systems --schema ./introspected-schema.graphql' - working_directory: 'packages/server' - store_test_results: path: packages/server/reports @@ -591,7 +575,7 @@ jobs: test-server-multiregion: <<: *test-server-job docker: - - image: cimg/node:18.19.0 + - image: cimg/node:22.6.0 - image: cimg/redis:7.2.4 - image: 'speckle/speckle-postgres' environment: diff --git a/.gitignore b/.gitignore index 7ef035683..10ff35f19 100644 --- a/.gitignore +++ b/.gitignore @@ -78,9 +78,11 @@ bin/ !packages/monitor-deployment/bin !packages/preview-service/bin !packages/server/bin +!packages/server/modules/cli/bin !packages/viewer/src/modules/loaders/OBJ # Server multiregion.json multiregion.test.json packages/*/.tshy/ +.vite-node \ No newline at end of file diff --git a/package.json b/package.json index e1717fb78..21f1b7006 100644 --- a/package.json +++ b/package.json @@ -95,7 +95,8 @@ "typescript": "^5.7.3", "typescript-eslint": "^8.20.0", "wait-on": ">=7.2.0", - "vitest": "^3.0.7" + "vitest": "^3.0.7", + "@types/node": "22.16.2" }, "config": { "commitizen": { diff --git a/packages/frontend-2/lib/viewer/composables/serialization.ts b/packages/frontend-2/lib/viewer/composables/serialization.ts index a2417dc84..2377687d8 100644 --- a/packages/frontend-2/lib/viewer/composables/serialization.ts +++ b/packages/frontend-2/lib/viewer/composables/serialization.ts @@ -13,7 +13,7 @@ import { import { CameraController, ViewMode, VisualDiffMode } from '@speckle/viewer' import type { NumericPropertyInfo } from '@speckle/viewer' import type { PartialDeep } from 'type-fest' -import type { SectionBoxData } from '@speckle/shared/dist/esm/viewer/helpers/state.js' +import type { SectionBoxData } from '@speckle/shared/viewer/state' type SerializedViewerState = SpeckleViewer.ViewerState.SerializedViewerState diff --git a/packages/frontend-2/lib/viewer/composables/setup.ts b/packages/frontend-2/lib/viewer/composables/setup.ts index 505ee477a..bd1173553 100644 --- a/packages/frontend-2/lib/viewer/composables/setup.ts +++ b/packages/frontend-2/lib/viewer/composables/setup.ts @@ -65,7 +65,7 @@ import { import { useSynchronizedCookie } from '~~/lib/common/composables/reactiveCookie' import { buildManualPromise } from '@speckle/ui-components' import { PassReader } from '../extensions/PassReader' -import type { SectionBoxData } from '@speckle/shared/dist/esm/viewer/helpers/state.js' +import type { SectionBoxData } from '@speckle/shared/viewer/state' export type LoadedModel = NonNullable< Get diff --git a/packages/frontend-2/lib/viewer/composables/setup/postSetup.ts b/packages/frontend-2/lib/viewer/composables/setup/postSetup.ts index a82681ea4..ec2c37e80 100644 --- a/packages/frontend-2/lib/viewer/composables/setup/postSetup.ts +++ b/packages/frontend-2/lib/viewer/composables/setup/postSetup.ts @@ -52,7 +52,7 @@ import { import { setupDebugMode } from '~~/lib/viewer/composables/setup/dev' import { useEmbed } from '~/lib/viewer/composables/setup/embed' import { useMixpanel } from '~~/lib/core/composables/mp' -import type { SectionBoxData } from '@speckle/shared/dist/esm/viewer/helpers/state.js' +import type { SectionBoxData } from '@speckle/shared/viewer/state' function useViewerIsBusyEventHandler() { const state = useInjectedViewerState() diff --git a/packages/server/.mocharc.js b/packages/server/.mocharc.cjs similarity index 100% rename from packages/server/.mocharc.js rename to packages/server/.mocharc.cjs diff --git a/packages/server/Dockerfile b/packages/server/Dockerfile index 8f1e660fb..d2b8526cc 100644 --- a/packages/server/Dockerfile +++ b/packages/server/Dockerfile @@ -73,6 +73,8 @@ COPY --link --from=dependency-stage /speckle-server/node_modules ./node_modules WORKDIR /speckle-server/packages/server COPY --link --from=build-stage /speckle-server/packages/server/package.json ./package.json +COPY --link --from=build-stage /speckle-server/packages/server/esmLoader.js ./esmLoader.js +COPY --link --from=build-stage /speckle-server/packages/server/root.js ./root.js COPY --link --from=build-stage /speckle-server/packages/server/dist ./dist COPY --link --from=build-stage /speckle-server/packages/server/assets ./assets COPY --link --from=build-stage /speckle-server/packages/server/bin ./bin @@ -86,4 +88,4 @@ ARG NODE_ENV ENV NODE_ENV=${NODE_ENV} \ SPECKLE_SERVER_VERSION=${SPECKLE_SERVER_VERSION} -ENTRYPOINT [ "tini", "--", "/nodejs/bin/node", "./bin/www" ] +ENTRYPOINT [ "tini", "--", "/nodejs/bin/node", "--import=./esmLoader.js", "./bin/www" ] diff --git a/packages/server/app.ts b/packages/server/app.ts index 3e987efcc..217bdb993 100644 --- a/packages/server/app.ts +++ b/packages/server/app.ts @@ -1,7 +1,7 @@ /* eslint-disable camelcase */ /* istanbul ignore file */ // eslint-disable-next-line no-restricted-imports -import './bootstrap' +import './bootstrap.js' import http from 'http' import express, { Express } from 'express' @@ -54,7 +54,7 @@ import { import * as ModulesSetup from '@/modules/index' import { GraphQLContext, Optional } from '@/modules/shared/helpers/typeHelper' -import { get, has, isString } from 'lodash' +import { get, has, isString } from 'lodash-es' import { corsMiddlewareFactory } from '@/modules/core/configs/cors' import { authContextMiddleware, @@ -103,17 +103,17 @@ const isWsServer = (server: http.Server | MockWsServer): server is MockWsServer * is that graphql-ws uses an entirely different protocol, so the client-side has to change as well, and so old clients * will be unable to use any WebSocket/subscriptions functionality with the updated server */ -export function buildApolloSubscriptionServer(params: { +export async function buildApolloSubscriptionServer(params: { server: http.Server | MockWsServer registers?: Registry[] -}): SubscriptionServer { +}): Promise { const { server, registers } = params const httpServer = isWsServer(server) ? undefined : server const mockServer = isWsServer(server) ? server : undefined // we have to break the type here, cause its a mock const wsServer = mockServer ? (mockServer as unknown as ws.Server) : undefined - const schema = ModulesSetup.graphSchema() + const schema = await ModulesSetup.graphSchema() const { metricConnectCounter, @@ -250,7 +250,7 @@ export async function buildApolloServer(options?: { }): Promise> { const includeStacktraceInErrorResponses = isDevEnv() || isTestEnv() const subscriptionServer = options?.subscriptionServer - const schema = ModulesSetup.graphSchema(await buildMocksConfig()) + const schema = await ModulesSetup.graphSchema(await buildMocksConfig()) const server = new ApolloServer({ schema, @@ -356,7 +356,7 @@ export async function init() { // Init HTTP server & subscription server const server = http.createServer(app) - const subscriptionServer = buildApolloSubscriptionServer({ + const subscriptionServer = await buildApolloSubscriptionServer({ server, registers: [promRegister] }) @@ -398,8 +398,7 @@ const shouldUseFrontendProxy = () => isDevEnv() async function createFrontendProxy() { const frontendHost = process.env.FRONTEND_HOST || '127.0.0.1' const frontendPort = process.env.FRONTEND_PORT || 8081 - const { createProxyMiddleware } = - require('http-proxy-middleware') as typeof import('http-proxy-middleware') + const { createProxyMiddleware } = await import('http-proxy-middleware') // even tho it has default values, it fixes http-proxy setting `Connection: close` on each request // slowing everything down diff --git a/packages/server/bin/gqlgen b/packages/server/bin/gqlgen new file mode 100755 index 000000000..568f8e7c5 --- /dev/null +++ b/packages/server/bin/gqlgen @@ -0,0 +1,13 @@ +#!/usr/bin/env node +import path from 'path' + +/** + * Find gqlgen and run it (we don't want to hardcode a specific node_modules path). + * We use this so that we can pass in specific flags to node before it even begins to run + */ +const relativeBinPath = './bin.js' + +const mochaPath = import.meta.resolve('@graphql-codegen/cli') +const mochaPathDir = path.dirname(mochaPath) +const mochaBinPath = path.join(mochaPathDir, relativeBinPath) +await import(mochaBinPath) diff --git a/packages/server/bin/mocha b/packages/server/bin/mocha index 8b4551adc..4646868fc 100755 --- a/packages/server/bin/mocha +++ b/packages/server/bin/mocha @@ -1,6 +1,5 @@ #!/usr/bin/env node -'use strict' -const path = require('path') +import path from 'path' /** * Find mocha and run it (we don't want to hardcode a specific node_modules path). @@ -8,7 +7,7 @@ const path = require('path') */ const relativeBinPath = './bin/mocha.js' -const mochaPath = require.resolve('mocha') +const mochaPath = import.meta.resolve('mocha') const mochaPathDir = path.dirname(mochaPath) const mochaBinPath = path.join(mochaPathDir, relativeBinPath) -require(mochaBinPath) +await import(mochaBinPath) diff --git a/packages/server/bin/ts-www b/packages/server/bin/ts-www deleted file mode 100755 index 361cbcb4a..000000000 --- a/packages/server/bin/ts-www +++ /dev/null @@ -1,24 +0,0 @@ -#!/usr/bin/env node -'use strict' - -/** - * Same as 'www', but runs the app from source code directly through ts-node, so no need to build the app into /dist first. - * Although ts-node with swc is pretty fast, in production environments you should use `www` and a built app. - */ - -require('ts-node/register') -const { logger } = require('../observability/logging') -const { init, startHttp } = require('../app') - -init() - .then(({ app, graphqlServer, registers, server, readinessCheck }) => - startHttp({ app, graphqlServer, registers, server, readinessCheck }) - ) - .catch((err) => { - logger.error(err, 'Failed to start server. Exiting with non-zero exit code...') - - // kill it with fire 🔥 - process.exit(1) - }) - -// 💥 diff --git a/packages/server/bin/www b/packages/server/bin/www index 31e1c2369..bf04f551e 100755 --- a/packages/server/bin/www +++ b/packages/server/bin/www @@ -1,8 +1,8 @@ #!/usr/bin/env node 'use strict' -const { logger } = require('../dist/observability/logging') -const { init, startHttp } = require('../dist/app') +import { logger } from '../dist/observability/logging.js' +import { init, startHttp } from '../dist/app.js' init() .then(({ app, graphqlServer, registers, server, readinessCheck }) => diff --git a/packages/server/bootstrap.js b/packages/server/bootstrap.js index 6ee2631d9..e2ec569fe 100644 --- a/packages/server/bootstrap.js +++ b/packages/server/bootstrap.js @@ -1,38 +1,28 @@ -/* istanbul ignore file */ +import dotenv from 'dotenv' +import { + isTestEnv, + isDevEnv, + isApolloMonitoringEnabled, + getApolloServerVersion, + getServerVersion +} from '@/modules/shared/helpers/envHelper' +import { logger } from '@/observability/logging' +import { initOpenTelemetry } from '@/observability/otel' +import { patchKnex } from '@/modules/core/patches/knex' +import { appRoot, packageRoot } from '#/root.js' +import inspector from 'node:inspector' + /** * Bootstrap module that should be imported at the very top of each entry point module */ -// Conditionally change appRoot and packageRoot according to whether we're running from /dist/ or not (ts-node) -const path = require('path') -const isTsNode = !!process[Symbol.for('ts-node.register.instance')] -const appRoot = __dirname -const packageRoot = isTsNode ? appRoot : path.resolve(__dirname, '../') - -// Initializing module aliases for absolute import paths -const moduleAlias = require('module-alias') -moduleAlias.addAliases({ - '@': appRoot, - '#': packageRoot -}) - // Initializing env vars -const dotenv = require('dotenv') -const { - isTestEnv, - isApolloMonitoringEnabled, - getApolloServerVersion, - getServerVersion, - isDevEnv -} = require('@/modules/shared/helpers/envHelper') -const { logger } = require('@/observability/logging') - if (isApolloMonitoringEnabled() && !getApolloServerVersion()) { process.env.APOLLO_SERVER_USER_VERSION = getServerVersion() } // If running in test env, load .env.test first -// (appRoot necessary, cause env files aren't loaded through require() calls) +// (appRoot necessary, cause env files aren't loaded through require()/import() calls) if (isTestEnv()) { const { error } = dotenv.config({ path: `${packageRoot}/.env.test` }) if (error) { @@ -48,7 +38,6 @@ if (isTestEnv()) { // (e.g. due to various child processes capturing the --inspect flag) const startDebugger = process.env.START_DEBUGGER if ((isTestEnv() || isDevEnv()) && startDebugger) { - const inspector = require('node:inspector') if (!inspector.url()) { console.log('Debugger starting on process ' + process.pid) inspector.open(0, undefined, true) @@ -58,13 +47,7 @@ if ((isTestEnv() || isDevEnv()) && startDebugger) { dotenv.config({ path: `${packageRoot}/.env` }) // knex is a singleton controlled by module so can't wait til app init -const { initOpenTelemetry } = require('@/observability/otel') initOpenTelemetry() - -const { patchKnex } = require('@/modules/core/patches/knex') patchKnex() -module.exports = { - appRoot, - packageRoot -} +export { appRoot, packageRoot } diff --git a/packages/server/codegen.ts b/packages/server/codegen.ts new file mode 100644 index 000000000..b715b8938 --- /dev/null +++ b/packages/server/codegen.ts @@ -0,0 +1,214 @@ +import type { CodegenConfig } from '@graphql-codegen/cli' + +const config: CodegenConfig = { + schema: ['modules/core/graph/schema.ts'], + overwrite: true, + documents: undefined, + generates: { + 'modules/core/graph/generated/graphql.ts': { + plugins: ['typescript', 'typescript-resolvers'], + config: { + enumsAsConst: true, + contextType: '@/modules/shared/helpers/typeHelper#GraphQLContext', + mappers: { + Stream: '@/modules/core/helpers/graphTypes#StreamGraphQLReturn', + Commit: '@/modules/core/helpers/graphTypes#CommitGraphQLReturn', + Project: '@/modules/core/helpers/graphTypes#ProjectGraphQLReturn', + Object: '@/modules/core/helpers/graphTypes#ObjectGraphQLReturn', + Version: '@/modules/core/helpers/graphTypes#VersionGraphQLReturn', + ServerInvite: + '@/modules/core/helpers/graphTypes#ServerInviteGraphQLReturnType', + Model: '@/modules/core/helpers/graphTypes#ModelGraphQLReturn', + ModelsTreeItem: + '@/modules/core/helpers/graphTypes#ModelsTreeItemGraphQLReturn', + StreamAccessRequest: + '@/modules/accessrequests/helpers/graphTypes#StreamAccessRequestGraphQLReturn', + ProjectAccessRequest: + '@/modules/accessrequests/helpers/graphTypes#ProjectAccessRequestGraphQLReturn', + ProjectAccessRequestMutations: + '@/modules/core/helpers/graphTypes#MutationsObjectGraphQLReturn', + LimitedUser: '@/modules/core/helpers/graphTypes#LimitedUserGraphQLReturn', + User: '@/modules/core/helpers/graphTypes#UserGraphQLReturn', + EmbedToken: '@/modules/core/helpers/graphTypes#EmbedTokenGraphQLReturn', + ActiveUserMutations: + '@/modules/core/helpers/graphTypes#MutationsObjectGraphQLReturn', + UserMetaMutations: + '@/modules/core/helpers/graphTypes#MutationsObjectGraphQLReturn', + UserEmailMutations: + '@/modules/core/helpers/graphTypes#MutationsObjectGraphQLReturn', + ProjectMutations: + '@/modules/core/helpers/graphTypes#MutationsObjectGraphQLReturn', + ProjectInviteMutations: + '@/modules/core/helpers/graphTypes#MutationsObjectGraphQLReturn', + ModelMutations: + '@/modules/core/helpers/graphTypes#MutationsObjectGraphQLReturn', + VersionMutations: + '@/modules/core/helpers/graphTypes#MutationsObjectGraphQLReturn', + FileUploadMutations: + '@/modules/core/helpers/graphTypes#MutationsObjectGraphQLReturn', + CommentMutations: + '@/modules/core/helpers/graphTypes#MutationsObjectGraphQLReturn', + AutomateMutations: + '@/modules/core/helpers/graphTypes#MutationsObjectGraphQLReturn', + AdminMutations: + '@/modules/core/helpers/graphTypes#MutationsObjectGraphQLReturn', + AdminQueries: '@/modules/core/helpers/graphTypes#GraphQLEmptyReturn', + ServerStatistics: '@/modules/core/helpers/graphTypes#GraphQLEmptyReturn', + ServerStats: '@/modules/core/helpers/graphTypes#GraphQLEmptyReturn', + CommentReplyAuthorCollection: + '@/modules/comments/helpers/graphTypes#CommentReplyAuthorCollectionGraphQLReturn', + Comment: '@/modules/comments/helpers/graphTypes#CommentGraphQLReturn', + CommentPermissionChecks: + '@/modules/comments/helpers/graphTypes#CommentPermissionChecksGraphQLReturn', + PendingStreamCollaborator: + '@/modules/serverinvites/helpers/graphTypes#PendingStreamCollaboratorGraphQLReturn', + StreamCollaborator: + '@/modules/core/helpers/graphTypes#StreamCollaboratorGraphQLReturn', + ProjectCollaborator: + '@/modules/core/helpers/graphTypes#ProjectCollaboratorGraphQLReturn', + FileUpload: '@/modules/fileuploads/helpers/types#FileUploadGraphQLReturn', + AutomateFunction: + '@/modules/automate/helpers/graphTypes#AutomateFunctionGraphQLReturn', + AutomateFunctionRelease: + '@/modules/automate/helpers/graphTypes#AutomateFunctionReleaseGraphQLReturn', + Automation: '@/modules/automate/helpers/graphTypes#AutomationGraphQLReturn', + AutomationPermissionChecks: + '@/modules/automate/helpers/graphTypes#AutomationPermissionChecksGraphQLReturn', + AutomationRevision: + '@/modules/automate/helpers/graphTypes#AutomationRevisionGraphQLReturn', + AutomationRevisionFunction: + '@/modules/automate/helpers/graphTypes#AutomationRevisionFunctionGraphQLReturn', + AutomateRun: '@/modules/automate/helpers/graphTypes#AutomateRunGraphQLReturn', + AutomationRunTrigger: + '@/modules/automate/helpers/graphTypes#AutomationRunTriggerGraphQLReturn', + VersionCreatedTrigger: + '@/modules/automate/helpers/graphTypes#AutomationRunTriggerGraphQLReturn', + AutomationRevisionTriggerDefinition: + '@/modules/automate/helpers/graphTypes#AutomationRevisionTriggerDefinitionGraphQLReturn', + VersionCreatedTriggerDefinition: + '@/modules/automate/helpers/graphTypes#AutomationRevisionTriggerDefinitionGraphQLReturn', + AutomateFunctionRun: + '@/modules/automate/helpers/graphTypes#AutomateFunctionRunGraphQLReturn', + TriggeredAutomationsStatus: + '@/modules/automate/helpers/graphTypes#TriggeredAutomationsStatusGraphQLReturn', + ProjectAutomationMutations: + '@/modules/automate/helpers/graphTypes#ProjectAutomationMutationsGraphQLReturn', + ProjectTriggeredAutomationsStatusUpdatedMessage: + '@/modules/automate/helpers/graphTypes#ProjectTriggeredAutomationsStatusUpdatedMessageGraphQLReturn', + ProjectAutomationsUpdatedMessage: + '@/modules/automate/helpers/graphTypes#ProjectAutomationsUpdatedMessageGraphQLReturn', + UserAutomateInfo: + '@/modules/automate/helpers/graphTypes#UserAutomateInfoGraphQLReturn', + Workspace: + '@/modules/workspacesCore/helpers/graphTypes#WorkspaceGraphQLReturn', + WorkspaceSso: + '@/modules/workspacesCore/helpers/graphTypes#WorkspaceSsoGraphQLReturn', + WorkspaceMutations: + '@/modules/workspacesCore/helpers/graphTypes#WorkspaceMutationsGraphQLReturn', + WorkspaceJoinRequestMutations: + '@/modules/workspacesCore/helpers/graphTypes#WorkspaceJoinRequestMutationsGraphQLReturn', + WorkspaceInviteMutations: + '@/modules/workspacesCore/helpers/graphTypes#WorkspaceInviteMutationsGraphQLReturn', + WorkspacePlan: + '@/modules/gatekeeperCore/helpers/graphTypes#WorkspacePlanGraphQLReturn', + WorkspacePlanUsage: + '@/modules/gatekeeperCore/helpers/graphTypes#WorkspacePlanUsageGraphQLReturn', + WorkspaceProjectMutations: + '@/modules/workspacesCore/helpers/graphTypes#WorkspaceProjectMutationsGraphQLReturn', + WorkspaceBillingMutations: + '@/modules/gatekeeper/helpers/graphTypes#WorkspaceBillingMutationsGraphQLReturn', + PendingWorkspaceCollaborator: + '@/modules/workspacesCore/helpers/graphTypes#PendingWorkspaceCollaboratorGraphQLReturn', + WorkspaceCollaborator: + '@/modules/workspacesCore/helpers/graphTypes#WorkspaceCollaboratorGraphQLReturn', + LimitedWorkspace: + '@/modules/workspacesCore/helpers/graphTypes#LimitedWorkspaceGraphQLReturn', + LimitedWorkspaceCollaborator: + '@/modules/workspacesCore/helpers/graphTypes#LimitedWorkspaceCollaboratorGraphQLReturn', + WorkspaceSubscriptionSeats: + '@/modules/gatekeeper/helpers/graphTypes#WorkspaceSubscriptionSeatsGraphQLReturn', + WorkspaceJoinRequest: + '@/modules/workspacesCore/helpers/graphTypes#WorkspaceJoinRequestGraphQLReturn', + LimitedWorkspaceJoinRequest: + '@/modules/workspacesCore/helpers/graphTypes#LimitedWorkspaceJoinRequestGraphQLReturn', + ProjectMoveToWorkspaceDryRun: + '@/modules/workspacesCore/helpers/graphTypes#ProjectMoveToWorkspaceDryRunGraphQLReturn', + Webhook: '@/modules/webhooks/helpers/graphTypes#WebhookGraphQLReturn', + SmartTextEditorValue: + '@/modules/core/services/richTextEditorService#SmartTextEditorValueGraphQLReturn', + BlobMetadata: '@/modules/blobstorage/domain/types#BlobStorageItem', + ServerWorkspacesInfo: '@/modules/core/helpers/graphTypes#GraphQLEmptyReturn', + ActivityCollection: + '@/modules/activitystream/helpers/graphTypes#ActivityCollectionGraphQLReturn', + ProjectRole: + '@/modules/workspacesCore/helpers/graphTypes#ProjectRoleGraphQLReturn', + ServerApp: '@/modules/auth/helpers/graphTypes#ServerAppGraphQLReturn', + ServerAppListItem: + '@/modules/auth/helpers/graphTypes#ServerAppListItemGraphQLReturn', + ServerInfo: '@/modules/core/helpers/graphTypes#ServerInfoGraphQLReturn', + Branch: '@/modules/core/helpers/graphTypes#BranchGraphQLReturn', + GendoAIRender: + '@/modules/gendo/helpers/types/graphTypes#GendoAIRenderGraphQLReturn', + ServerMultiRegionConfiguration: + '@/modules/core/helpers/graphTypes#GraphQLEmptyReturn', + ServerInfoMutations: + '@/modules/core/helpers/graphTypes#MutationsObjectGraphQLReturn', + ServerRegionMutations: + '@/modules/core/helpers/graphTypes#MutationsObjectGraphQLReturn', + ServerRegionItem: + '@/modules/multiregion/helpers/graphTypes#ServerRegionItemGraphQLReturn', + Price: '@/modules/gatekeeperCore/helpers/graphTypes#PriceGraphQLReturn', + WorkspaceSubscription: + '@/modules/gatekeeper/helpers/graphTypes#WorkspaceSubscriptionGraphQLReturn', + UserMeta: '@/modules/core/helpers/graphTypes#UserMetaGraphQLReturn', + ProjectPermissionChecks: + '@/modules/core/helpers/graphTypes#ProjectPermissionChecksGraphQLReturn', + ModelPermissionChecks: + '@/modules/core/helpers/graphTypes#ModelPermissionChecksGraphQLReturn', + VersionPermissionChecks: + '@/modules/core/helpers/graphTypes#VersionPermissionChecksGraphQLReturn', + RootPermissionChecks: + '@/modules/core/helpers/graphTypes#RootPermissionChecksGraphQLReturn', + WorkspacePermissionChecks: + '@/modules/workspacesCore/helpers/graphTypes#WorkspacePermissionChecksGraphQLReturn' + } + } + }, + 'modules/cross-server-sync/graph/generated/graphql.ts': { + plugins: ['typescript', 'typescript-operations'], + documents: ['modules/cross-server-sync/**/*.{js,ts}'], + config: { + enumsAsConst: true, + scalars: { + JSONObject: 'Record', + DateTime: 'string' + } + } + }, + 'test/graphql/generated/graphql.ts': { + plugins: ['typescript', 'typescript-operations', 'typed-document-node'], + documents: [ + 'test/graphql/*.{js,ts}', + 'modules/**/tests/helpers/graphql.ts', + 'modules/**/tests/helpers/*Graphql.ts', + 'modules/**/tests/helpers/graphql/*.ts' + ], + config: { + enumsAsConst: true, + scalars: { + JSONObject: 'Record', + DateTime: 'string' + } + } + } + }, + config: { + enumsAsConst: true, + scalars: { + JSONObject: 'Record', + DateTime: 'Date' + } + } +} + +export default config diff --git a/packages/server/codegen.yml b/packages/server/codegen.yml deleted file mode 100644 index 5bd42aa19..000000000 --- a/packages/server/codegen.yml +++ /dev/null @@ -1,139 +0,0 @@ -overwrite: true -schema: - - 'modules/schema.ts' -documents: null -generates: - modules/core/graph/generated/graphql.ts: - plugins: - - 'typescript' - - 'typescript-resolvers' - config: - enumsAsConst: true - contextType: '@/modules/shared/helpers/typeHelper#GraphQLContext' - mappers: - Stream: '@/modules/core/helpers/graphTypes#StreamGraphQLReturn' - Commit: '@/modules/core/helpers/graphTypes#CommitGraphQLReturn' - Project: '@/modules/core/helpers/graphTypes#ProjectGraphQLReturn' - Object: '@/modules/core/helpers/graphTypes#ObjectGraphQLReturn' - Version: '@/modules/core/helpers/graphTypes#VersionGraphQLReturn' - ServerInvite: '@/modules/core/helpers/graphTypes#ServerInviteGraphQLReturnType' - Model: '@/modules/core/helpers/graphTypes#ModelGraphQLReturn' - ModelsTreeItem: '@/modules/core/helpers/graphTypes#ModelsTreeItemGraphQLReturn' - StreamAccessRequest: '@/modules/accessrequests/helpers/graphTypes#StreamAccessRequestGraphQLReturn' - ProjectAccessRequest: '@/modules/accessrequests/helpers/graphTypes#ProjectAccessRequestGraphQLReturn' - ProjectAccessRequestMutations: '@/modules/core/helpers/graphTypes#MutationsObjectGraphQLReturn' - LimitedUser: '@/modules/core/helpers/graphTypes#LimitedUserGraphQLReturn' - User: '@/modules/core/helpers/graphTypes#UserGraphQLReturn' - EmbedToken: '@/modules/core/helpers/graphTypes#EmbedTokenGraphQLReturn' - ActiveUserMutations: '@/modules/core/helpers/graphTypes#MutationsObjectGraphQLReturn' - UserMetaMutations: '@/modules/core/helpers/graphTypes#MutationsObjectGraphQLReturn' - UserEmailMutations: '@/modules/core/helpers/graphTypes#MutationsObjectGraphQLReturn' - ProjectMutations: '@/modules/core/helpers/graphTypes#MutationsObjectGraphQLReturn' - ProjectInviteMutations: '@/modules/core/helpers/graphTypes#MutationsObjectGraphQLReturn' - ModelMutations: '@/modules/core/helpers/graphTypes#MutationsObjectGraphQLReturn' - VersionMutations: '@/modules/core/helpers/graphTypes#MutationsObjectGraphQLReturn' - FileUploadMutations: '@/modules/core/helpers/graphTypes#MutationsObjectGraphQLReturn' - CommentMutations: '@/modules/core/helpers/graphTypes#MutationsObjectGraphQLReturn' - AutomateMutations: '@/modules/core/helpers/graphTypes#MutationsObjectGraphQLReturn' - AdminMutations: '@/modules/core/helpers/graphTypes#MutationsObjectGraphQLReturn' - AdminQueries: '@/modules/core/helpers/graphTypes#GraphQLEmptyReturn' - ServerStatistics: '@/modules/core/helpers/graphTypes#GraphQLEmptyReturn' - ServerStats: '@/modules/core/helpers/graphTypes#GraphQLEmptyReturn' - CommentReplyAuthorCollection: '@/modules/comments/helpers/graphTypes#CommentReplyAuthorCollectionGraphQLReturn' - Comment: '@/modules/comments/helpers/graphTypes#CommentGraphQLReturn' - CommentPermissionChecks: '@/modules/comments/helpers/graphTypes#CommentPermissionChecksGraphQLReturn' - PendingStreamCollaborator: '@/modules/serverinvites/helpers/graphTypes#PendingStreamCollaboratorGraphQLReturn' - StreamCollaborator: '@/modules/core/helpers/graphTypes#StreamCollaboratorGraphQLReturn' - ProjectCollaborator: '@/modules/core/helpers/graphTypes#ProjectCollaboratorGraphQLReturn' - FileUpload: '@/modules/fileuploads/helpers/types#FileUploadGraphQLReturn' - AutomateFunction: '@/modules/automate/helpers/graphTypes#AutomateFunctionGraphQLReturn' - AutomateFunctionRelease: '@/modules/automate/helpers/graphTypes#AutomateFunctionReleaseGraphQLReturn' - Automation: '@/modules/automate/helpers/graphTypes#AutomationGraphQLReturn' - AutomationPermissionChecks: '@/modules/automate/helpers/graphTypes#AutomationPermissionChecksGraphQLReturn' - AutomationRevision: '@/modules/automate/helpers/graphTypes#AutomationRevisionGraphQLReturn' - AutomationRevisionFunction: '@/modules/automate/helpers/graphTypes#AutomationRevisionFunctionGraphQLReturn' - AutomateRun: '@/modules/automate/helpers/graphTypes#AutomateRunGraphQLReturn' - AutomationRunTrigger: '@/modules/automate/helpers/graphTypes#AutomationRunTriggerGraphQLReturn' - VersionCreatedTrigger: '@/modules/automate/helpers/graphTypes#AutomationRunTriggerGraphQLReturn' - AutomationRevisionTriggerDefinition: '@/modules/automate/helpers/graphTypes#AutomationRevisionTriggerDefinitionGraphQLReturn' - VersionCreatedTriggerDefinition: '@/modules/automate/helpers/graphTypes#AutomationRevisionTriggerDefinitionGraphQLReturn' - AutomateFunctionRun: '@/modules/automate/helpers/graphTypes#AutomateFunctionRunGraphQLReturn' - TriggeredAutomationsStatus: '@/modules/automate/helpers/graphTypes#TriggeredAutomationsStatusGraphQLReturn' - ProjectAutomationMutations: '@/modules/automate/helpers/graphTypes#ProjectAutomationMutationsGraphQLReturn' - ProjectTriggeredAutomationsStatusUpdatedMessage: '@/modules/automate/helpers/graphTypes#ProjectTriggeredAutomationsStatusUpdatedMessageGraphQLReturn' - ProjectAutomationsUpdatedMessage: '@/modules/automate/helpers/graphTypes#ProjectAutomationsUpdatedMessageGraphQLReturn' - UserAutomateInfo: '@/modules/automate/helpers/graphTypes#UserAutomateInfoGraphQLReturn' - Workspace: '@/modules/workspacesCore/helpers/graphTypes#WorkspaceGraphQLReturn' - WorkspaceSso: '@/modules/workspacesCore/helpers/graphTypes#WorkspaceSsoGraphQLReturn' - WorkspaceMutations: '@/modules/workspacesCore/helpers/graphTypes#WorkspaceMutationsGraphQLReturn' - WorkspaceJoinRequestMutations: '@/modules/workspacesCore/helpers/graphTypes#WorkspaceJoinRequestMutationsGraphQLReturn' - WorkspaceInviteMutations: '@/modules/workspacesCore/helpers/graphTypes#WorkspaceInviteMutationsGraphQLReturn' - WorkspacePlan: '@/modules/gatekeeperCore/helpers/graphTypes#WorkspacePlanGraphQLReturn' - WorkspacePlanUsage: '@/modules/gatekeeperCore/helpers/graphTypes#WorkspacePlanUsageGraphQLReturn' - WorkspaceProjectMutations: '@/modules/workspacesCore/helpers/graphTypes#WorkspaceProjectMutationsGraphQLReturn' - WorkspaceBillingMutations: '@/modules/gatekeeper/helpers/graphTypes#WorkspaceBillingMutationsGraphQLReturn' - PendingWorkspaceCollaborator: '@/modules/workspacesCore/helpers/graphTypes#PendingWorkspaceCollaboratorGraphQLReturn' - WorkspaceCollaborator: '@/modules/workspacesCore/helpers/graphTypes#WorkspaceCollaboratorGraphQLReturn' - LimitedWorkspace: '@/modules/workspacesCore/helpers/graphTypes#LimitedWorkspaceGraphQLReturn' - LimitedWorkspaceCollaborator: '@/modules/workspacesCore/helpers/graphTypes#LimitedWorkspaceCollaboratorGraphQLReturn' - WorkspaceSubscriptionSeats: '@/modules/gatekeeper/helpers/graphTypes#WorkspaceSubscriptionSeatsGraphQLReturn' - WorkspaceJoinRequest: '@/modules/workspacesCore/helpers/graphTypes#WorkspaceJoinRequestGraphQLReturn' - LimitedWorkspaceJoinRequest: '@/modules/workspacesCore/helpers/graphTypes#LimitedWorkspaceJoinRequestGraphQLReturn' - ProjectMoveToWorkspaceDryRun: '@/modules/workspacesCore/helpers/graphTypes#ProjectMoveToWorkspaceDryRunGraphQLReturn' - Webhook: '@/modules/webhooks/helpers/graphTypes#WebhookGraphQLReturn' - SmartTextEditorValue: '@/modules/core/services/richTextEditorService#SmartTextEditorValueGraphQLReturn' - BlobMetadata: '@/modules/blobstorage/domain/types#BlobStorageItem' - ServerWorkspacesInfo: '@/modules/core/helpers/graphTypes#GraphQLEmptyReturn' - ActivityCollection: '@/modules/activitystream/helpers/graphTypes#ActivityCollectionGraphQLReturn' - ProjectRole: '@/modules/workspacesCore/helpers/graphTypes#ProjectRoleGraphQLReturn' - ServerApp: '@/modules/auth/helpers/graphTypes#ServerAppGraphQLReturn' - ServerAppListItem: '@/modules/auth/helpers/graphTypes#ServerAppListItemGraphQLReturn' - ServerInfo: '@/modules/core/helpers/graphTypes#ServerInfoGraphQLReturn' - Branch: '@/modules/core/helpers/graphTypes#BranchGraphQLReturn' - GendoAIRender: '@/modules/gendo/helpers/types/graphTypes#GendoAIRenderGraphQLReturn' - ServerMultiRegionConfiguration: '@/modules/core/helpers/graphTypes#GraphQLEmptyReturn' - ServerInfoMutations: '@/modules/core/helpers/graphTypes#MutationsObjectGraphQLReturn' - ServerRegionMutations: '@/modules/core/helpers/graphTypes#MutationsObjectGraphQLReturn' - ServerRegionItem: '@/modules/multiregion/helpers/graphTypes#ServerRegionItemGraphQLReturn' - Price: '@/modules/gatekeeperCore/helpers/graphTypes#PriceGraphQLReturn' - WorkspaceSubscription: '@/modules/gatekeeper/helpers/graphTypes#WorkspaceSubscriptionGraphQLReturn' - UserMeta: '@/modules/core/helpers/graphTypes#UserMetaGraphQLReturn' - ProjectPermissionChecks: '@/modules/core/helpers/graphTypes#ProjectPermissionChecksGraphQLReturn' - ModelPermissionChecks: '@/modules/core/helpers/graphTypes#ModelPermissionChecksGraphQLReturn' - VersionPermissionChecks: '@/modules/core/helpers/graphTypes#VersionPermissionChecksGraphQLReturn' - RootPermissionChecks: '@/modules/core/helpers/graphTypes#RootPermissionChecksGraphQLReturn' - WorkspacePermissionChecks: '@/modules/workspacesCore/helpers/graphTypes#WorkspacePermissionChecksGraphQLReturn' - modules/cross-server-sync/graph/generated/graphql.ts: - plugins: - - 'typescript' - - 'typescript-operations' - documents: - - 'modules/cross-server-sync/**/*.{js,ts}' - config: - enumsAsConst: true - scalars: - JSONObject: Record - DateTime: string - test/graphql/generated/graphql.ts: - plugins: - - 'typescript' - - 'typescript-operations' - - 'typed-document-node' - documents: - - 'test/graphql/*.{js,ts}' - - 'modules/**/tests/helpers/graphql.ts' - - 'modules/**/tests/helpers/*Graphql.ts' - - 'modules/**/tests/helpers/graphql/*.ts' - config: - enumsAsConst: true - scalars: - JSONObject: Record - DateTime: string -config: - enumsAsConst: true - scalars: - JSONObject: Record - DateTime: Date -require: - - ts-node/register - - tsconfig-paths/register diff --git a/packages/server/eslint.config.mjs b/packages/server/eslint.config.mjs index 0b654235b..49c425ea3 100644 --- a/packages/server/eslint.config.mjs +++ b/packages/server/eslint.config.mjs @@ -13,24 +13,24 @@ const configs = [ ...baseConfigs, { languageOptions: { - sourceType: 'commonjs', + sourceType: 'module', globals: { ...globals.node } } }, { - files: ['**/*.mjs'], + files: ['**/*.cjs', '**/*.cts'], languageOptions: { - sourceType: 'module' + sourceType: 'commonjs' } }, ...tseslint.configs.recommendedTypeChecked.map((c) => ({ ...c, - files: [...(c.files || []), '**/*.ts', '**/*.d.ts'] + files: [...(c.files || []), '**/*.ts', '**/*.d.ts', '**/*.cts'] })), { - files: ['**/*.ts', '**/*.d.ts'], + files: ['**/*.ts', '**/*.d.ts', '**/*.cts'], languageOptions: { parserOptions: { tsconfigRootDir: getESMDirname(import.meta.url), diff --git a/packages/server/esmLoader.js b/packages/server/esmLoader.js new file mode 100644 index 000000000..d541ed272 --- /dev/null +++ b/packages/server/esmLoader.js @@ -0,0 +1,84 @@ +import path from 'node:path' +import { pathToFileURL } from 'node:url' +import { register } from 'node:module' +import { appRoot, packageRoot } from './root.js' + +/** + * Must be invoked through --import when running the node app to set up the following: + * - Custom path aliases for imports + * - Extensionless imports + * - Directory imports like in CJS + */ + +/** + * PATH ALIAS DEFINITIONS + */ +const aliases = { + '@/': appRoot + '/', + '#/': packageRoot + '/' +} + +/** + * EXTENSIONS TO EVALUATE FOR EXTENSIONLESS IMPORTS + */ +const extensions = ['.js', '.mjs', '.cjs', '.json'] + +// Register the module hooks +register('./esmLoader.js', { + parentURL: import.meta.url +}) + +// Custom path resolver +function resolveAlias(specifier) { + for (const [alias, target] of Object.entries(aliases)) { + if (specifier.startsWith(alias)) { + const relativePath = specifier.replace(alias, target) + return pathToFileURL(path.resolve(relativePath)).href + } + } + return null // No alias found, fall back to default resolution +} + +/** + * Adjust global ESM resolution logic to allow for path/package aliases, dir imports and extensionless imports + */ +export async function resolve(specifier, _context, nextResolve) { + // Resolve alias + const aliasResolved = resolveAlias(specifier) + specifier = aliasResolved || specifier + + // Try to resolve as is + let throwableError = undefined + try { + return await nextResolve(specifier) + } catch (e) { + throwableError = e + } + + const isDirImport = throwableError.code === 'ERR_UNSUPPORTED_DIR_IMPORT' + + // Didn't work, try with extensions + for (const ext of extensions) { + try { + return await nextResolve(specifier + ext) + } catch (e) { + if (!throwableError) { + throwableError = e + } + } + } + + // If it was a dir import also, try that with extensions + specifier = isDirImport ? path.join(specifier, 'index') : specifier + for (const ext of extensions) { + try { + return await nextResolve(specifier + ext) + } catch (e) { + if (!throwableError) { + throwableError = e + } + } + } + + throw throwableError +} diff --git a/packages/server/healthchecks/health.ts b/packages/server/healthchecks/health.ts index b0cefb4d0..3ff2d8bff 100644 --- a/packages/server/healthchecks/health.ts +++ b/packages/server/healthchecks/health.ts @@ -1,6 +1,6 @@ import { ensureErrorOrWrapAsCause } from '@/modules/shared/errors/ensureError' -import { join, merge } from 'lodash' -import { MultiError } from 'verror' +import { join, merge } from 'lodash-es' +import VError from 'verror' import { FreeConnectionsCalculators, MultiDBCheck, @@ -30,7 +30,7 @@ export const handleLivenessFactory = ', ' )} is not available.`, { - cause: new MultiError( + cause: new VError.MultiError( Object.entries(allPostgresResults).map((kv) => ensureErrorOrWrapAsCause( //HACK: kv[1] is not typed correctly as the filter does not narrow the type diff --git a/packages/server/knexfile.ts b/packages/server/knexfile.ts index a63351c99..48d47e380 100644 --- a/packages/server/knexfile.ts +++ b/packages/server/knexfile.ts @@ -1,6 +1,6 @@ /* eslint-disable no-restricted-imports */ /* istanbul ignore file */ -import { packageRoot } from './bootstrap' +import { packageRoot } from './bootstrap.js' import fs from 'fs' import path from 'path' import { diff --git a/packages/server/modules/accessrequests/graph/resolvers/index.ts b/packages/server/modules/accessrequests/graph/resolvers/index.ts index e60565270..bf7b2611c 100644 --- a/packages/server/modules/accessrequests/graph/resolvers/index.ts +++ b/packages/server/modules/accessrequests/graph/resolvers/index.ts @@ -268,4 +268,4 @@ const resolvers: Resolvers = { } } -export = resolvers +export default resolvers diff --git a/packages/server/modules/accessrequests/index.ts b/packages/server/modules/accessrequests/index.ts index 5015d0f5f..59b9f56d0 100644 --- a/packages/server/modules/accessrequests/index.ts +++ b/packages/server/modules/accessrequests/index.ts @@ -26,4 +26,4 @@ const ServerAccessRequestsModule: SpeckleModule = { } } -export = ServerAccessRequestsModule +export default ServerAccessRequestsModule diff --git a/packages/server/modules/accessrequests/tests/projectAccessRequests.spec.ts b/packages/server/modules/accessrequests/tests/projectAccessRequests.spec.ts index 0dfebe498..bd81bc96c 100644 --- a/packages/server/modules/accessrequests/tests/projectAccessRequests.spec.ts +++ b/packages/server/modules/accessrequests/tests/projectAccessRequests.spec.ts @@ -51,15 +51,15 @@ import { } from '@/test/graphql/generated/graphql' import { testApolloServer, TestApolloServer } from '@/test/graphqlHelper' import { truncateTables } from '@/test/hooks' -import { EmailSendingServiceMock } from '@/test/mocks/global' import { buildNotificationsStateTracker, NotificationsStateManager } from '@/test/notificationsHelper' import { getStreamActivities } from '@/test/speckle-helpers/activityStreamHelper' +import { createEmailListener, TestEmailListener } from '@/test/speckle-helpers/email' import { BasicTestStream, createTestStreams } from '@/test/speckle-helpers/streamHelper' import { expect } from 'chai' -import { noop } from 'lodash' +import { noop } from 'lodash-es' const getUser = getUserFactory({ db }) const getStream = getStreamFactory({ db }) @@ -150,6 +150,8 @@ describe('Project access requests', () => { id: '' } + let emailListener: TestEmailListener + let quitters: (() => void)[] = [] before(async () => { @@ -164,15 +166,18 @@ describe('Project access requests', () => { authUserId: me.id }) notificationsStateManager = buildNotificationsStateTracker() + emailListener = await createEmailListener() }) afterEach(() => { + emailListener.reset() quitters.forEach((q) => q()) quitters = [] }) after(async () => { notificationsStateManager.destroy() + await emailListener.destroy() }) const createReq = async (projectId: string) => @@ -228,10 +233,8 @@ describe('Project access requests', () => { eventFired = true }) ) - const sendEmailCall = EmailSendingServiceMock.hijackFunction( - 'sendEmail', - async () => true - ) + + const { getSends } = emailListener.listen({ times: 1 }) const waitForAck = notificationsStateManager.waitForAck( (e) => e.result?.type === NotificationType.NewStreamAccessRequest @@ -255,8 +258,9 @@ describe('Project access requests', () => { await waitForAck // email gets sent out - expect(sendEmailCall.args?.[0]?.[0]).to.be.ok - const emailParams = sendEmailCall.args[0][0] + const sentEmails = getSends() + expect(sentEmails.length).to.eq(1) + const emailParams = sentEmails[0] expect(emailParams.subject).to.contain('A user requested access to your project') expect(emailParams.html).to.be.ok diff --git a/packages/server/modules/accessrequests/tests/streamAccessRequests.spec.ts b/packages/server/modules/accessrequests/tests/streamAccessRequests.spec.ts index c15ec29c3..889d69d05 100644 --- a/packages/server/modules/accessrequests/tests/streamAccessRequests.spec.ts +++ b/packages/server/modules/accessrequests/tests/streamAccessRequests.spec.ts @@ -52,15 +52,15 @@ import { import { StreamRole } from '@/test/graphql/generated/graphql' import { createAuthedTestContext, ServerAndContext } from '@/test/graphqlHelper' import { truncateTables } from '@/test/hooks' -import { EmailSendingServiceMock } from '@/test/mocks/global' import { buildNotificationsStateTracker, NotificationsStateManager } from '@/test/notificationsHelper' import { getStreamActivities } from '@/test/speckle-helpers/activityStreamHelper' +import { createEmailListener, TestEmailListener } from '@/test/speckle-helpers/email' import { BasicTestStream, createTestStreams } from '@/test/speckle-helpers/streamHelper' import { expect } from 'chai' -import { noop } from 'lodash' +import { noop } from 'lodash-es' const getUser = getUserFactory({ db }) const getStreamCollaborators = getStreamCollaboratorsFactory({ db }) @@ -154,6 +154,8 @@ describe('Stream access requests', () => { id: '' } + let emailListener: TestEmailListener + before(async () => { await cleanup() await createTestUsers([me, otherGuy, anotherGuy]) @@ -167,10 +169,16 @@ describe('Stream access requests', () => { context: await createAuthedTestContext(me.id) } notificationsStateManager = buildNotificationsStateTracker() + emailListener = await createEmailListener() }) after(async () => { notificationsStateManager.destroy() + await emailListener.destroy() + }) + + afterEach(async () => { + emailListener.reset() }) const createReq = (streamId: string) => @@ -202,10 +210,7 @@ describe('Stream access requests', () => { }) it('operation succeeds', async () => { - const sendEmailCall = EmailSendingServiceMock.hijackFunction( - 'sendEmail', - async () => true - ) + const { getSends } = emailListener.listen({ times: 1 }) const waitForAck = notificationsStateManager.waitForAck( (e) => e.result?.type === NotificationType.NewStreamAccessRequest @@ -226,8 +231,9 @@ describe('Stream access requests', () => { await waitForAck // email gets sent out - expect(sendEmailCall.args?.[0]?.[0]).to.be.ok - const emailParams = sendEmailCall.args[0][0] + const sentEmails = getSends() + expect(sentEmails.length).to.eq(1) + const emailParams = sentEmails[0] expect(emailParams.subject).to.contain('A user requested access to your project') expect(emailParams.html).to.be.ok diff --git a/packages/server/modules/activitystream/events/commentListeners.ts b/packages/server/modules/activitystream/events/commentListeners.ts index 44b004933..31ed82cec 100644 --- a/packages/server/modules/activitystream/events/commentListeners.ts +++ b/packages/server/modules/activitystream/events/commentListeners.ts @@ -15,7 +15,7 @@ import { import { CommentEvents, CommentEventsPayloads } from '@/modules/comments/domain/events' import { ReplyCreateInput } from '@/modules/core/graph/generated/graphql' import { EventBusListen } from '@/modules/shared/services/eventBus' -import { has } from 'lodash' +import { has } from 'lodash-es' import { OverrideProperties } from 'type-fest' const addThreadCreatedActivityFactory = diff --git a/packages/server/modules/activitystream/graph/resolvers/activity.ts b/packages/server/modules/activitystream/graph/resolvers/activity.ts index a75766f50..3b2191146 100644 --- a/packages/server/modules/activitystream/graph/resolvers/activity.ts +++ b/packages/server/modules/activitystream/graph/resolvers/activity.ts @@ -65,7 +65,7 @@ const userTimelineQueryCore = async ( return { items, cursor, totalCount } } -export = { +export default { LimitedUser: { async activity(parent, args) { return await userActivityQueryCore(parent, args) diff --git a/packages/server/modules/activitystream/index.ts b/packages/server/modules/activitystream/index.ts index 7b4ee02f4..b6ebef707 100644 --- a/packages/server/modules/activitystream/index.ts +++ b/packages/server/modules/activitystream/index.ts @@ -154,6 +154,6 @@ const activityModule: SpeckleModule = { } } -export = { +export default { ...activityModule } diff --git a/packages/server/modules/activitystream/migrations/20210616173000_stream_activity.js b/packages/server/modules/activitystream/migrations/20210616173000_stream_activity.js index febbaf874..96e204178 100644 --- a/packages/server/modules/activitystream/migrations/20210616173000_stream_activity.js +++ b/packages/server/modules/activitystream/migrations/20210616173000_stream_activity.js @@ -1,5 +1,5 @@ // /* istanbul ignore file */ -exports.up = async (knex) => { +const up = async (knex) => { await knex.schema.createTable('stream_activity', (table) => { // No foreign keys because the referenced objects may be deleted, but we want to keep their ids here in this table for future analysis table.string('streamId', 10) @@ -18,6 +18,8 @@ exports.up = async (knex) => { }) } -exports.down = async (knex) => { +const down = async (knex) => { await knex.schema.dropTableIfExists('stream_activity') } + +export { up, down } diff --git a/packages/server/modules/apiexplorer/index.ts b/packages/server/modules/apiexplorer/index.ts index cb8cb37eb..69404c70d 100644 --- a/packages/server/modules/apiexplorer/index.ts +++ b/packages/server/modules/apiexplorer/index.ts @@ -2,10 +2,11 @@ import { SpeckleModule } from '@/modules/shared/helpers/typeHelper' import { moduleLogger } from '@/observability/logging' import { readFile } from 'fs/promises' import { getFrontendOrigin } from '@/modules/shared/helpers/envHelper' +import { fileURLToPath } from 'url' async function getExplorerHtml() { const fileBaseContents = await readFile( - require.resolve('#/assets/apiexplorer/templates/explorer.html'), + fileURLToPath(import.meta.resolve('#/assets/apiexplorer/templates/explorer.html')), { encoding: 'utf-8' } ) return fileBaseContents.replace( diff --git a/packages/server/modules/auth/graph/resolvers/apps.ts b/packages/server/modules/auth/graph/resolvers/apps.ts index 0b03faa72..e1a803c58 100644 --- a/packages/server/modules/auth/graph/resolvers/apps.ts +++ b/packages/server/modules/auth/graph/resolvers/apps.ts @@ -25,7 +25,7 @@ const revokeExistingAppCredentialsForUser = revokeExistingAppCredentialsForUserF db }) -export = { +export default { Query: { async app(_parent, args) { const app = await getApp({ id: args.id }) diff --git a/packages/server/modules/auth/graph/resolvers/auth.ts b/packages/server/modules/auth/graph/resolvers/auth.ts index 30254b671..b6a18a09d 100644 --- a/packages/server/modules/auth/graph/resolvers/auth.ts +++ b/packages/server/modules/auth/graph/resolvers/auth.ts @@ -1,7 +1,7 @@ import { getAuthStrategies } from '@/modules/auth' import { Resolvers } from '@/modules/core/graph/generated/graphql' -export = { +export default { ServerInfo: { authStrategies() { return getAuthStrategies() diff --git a/packages/server/modules/auth/helpers/oidc.spec.ts b/packages/server/modules/auth/helpers/oidc.spec.ts index 6db18e5ba..b2fa068bd 100644 --- a/packages/server/modules/auth/helpers/oidc.spec.ts +++ b/packages/server/modules/auth/helpers/oidc.spec.ts @@ -1,5 +1,4 @@ import { expect } from 'chai' -import { describe, it } from 'mocha' import { getNameFromUserInfo } from '@/modules/auth/helpers/oidc' /* eslint-disable camelcase */ diff --git a/packages/server/modules/auth/middleware.ts b/packages/server/modules/auth/middleware.ts index 6c3bfa86d..28f24f6c8 100644 --- a/packages/server/modules/auth/middleware.ts +++ b/packages/server/modules/auth/middleware.ts @@ -8,7 +8,7 @@ import { getFrontendOrigin, getSessionSecret } from '@/modules/shared/helpers/envHelper' -import { isString, noop } from 'lodash' +import { isString, noop } from 'lodash-es' import { CreateAuthorizationCode } from '@/modules/auth/domain/operations' import { ensureError, TIME_MS } from '@speckle/shared' import { LegacyGetUser } from '@/modules/core/domain/users/operations' diff --git a/packages/server/modules/auth/migrations/2020-05-29-apps.js b/packages/server/modules/auth/migrations/2020-05-29-apps.js index 5b3a8ecdb..391dce19d 100644 --- a/packages/server/modules/auth/migrations/2020-05-29-apps.js +++ b/packages/server/modules/auth/migrations/2020-05-29-apps.js @@ -2,7 +2,7 @@ 'use strict' // Knex table migrations -exports.up = async (knex) => { +const up = async (knex) => { // Applications that integrate with this server. await knex.schema.createTable('server_apps', (table) => { table.string('id', 10).primary() @@ -93,7 +93,7 @@ exports.up = async (knex) => { // await knex( 'scopes' ).insert( appTokenScopes ) } -exports.down = async (knex) => { +const down = async (knex) => { await knex.schema.dropTableIfExists('server_apps_scopes') await knex.schema.dropTableIfExists('authorization_codes') await knex.schema.dropTableIfExists('refresh_tokens') @@ -101,3 +101,5 @@ exports.down = async (knex) => { await knex.schema.dropTableIfExists('server_apps') } + +export { up, down } diff --git a/packages/server/modules/auth/repositories/apps.ts b/packages/server/modules/auth/repositories/apps.ts index 3008a904b..81465aa6a 100644 --- a/packages/server/modules/auth/repositories/apps.ts +++ b/packages/server/modules/auth/repositories/apps.ts @@ -46,7 +46,7 @@ import { import { ServerAppRecord, UserRecord } from '@/modules/core/helpers/types' import cryptoRandomString from 'crypto-random-string' import { Knex } from 'knex' -import { difference, omit } from 'lodash' +import { difference, omit } from 'lodash-es' import { AppCreateError } from '@/modules/auth/errors' import { UserInputError } from '@/modules/core/errors/userinput' diff --git a/packages/server/modules/auth/repositories/index.ts b/packages/server/modules/auth/repositories/index.ts index f03b6ad80..185028718 100644 --- a/packages/server/modules/auth/repositories/index.ts +++ b/packages/server/modules/auth/repositories/index.ts @@ -8,7 +8,7 @@ import { import { InvalidArgumentError } from '@/modules/shared/errors' import { Nullable } from '@/modules/shared/helpers/typeHelper' import { ServerAppsScopesRecord } from '@/modules/auth/helpers/types' -import { groupBy, mapValues } from 'lodash' +import { groupBy, mapValues } from 'lodash-es' import { TokenScopeData } from '@/modules/shared/domain/rolesAndScopes/types' import { Knex } from 'knex' import { diff --git a/packages/server/modules/auth/services/passportService.ts b/packages/server/modules/auth/services/passportService.ts index 8f7e10ef6..78df3cf69 100644 --- a/packages/server/modules/auth/services/passportService.ts +++ b/packages/server/modules/auth/services/passportService.ts @@ -2,7 +2,7 @@ import type { Strategy, AuthenticateOptions } from 'passport' import passport from 'passport' import type { Request, Response, NextFunction, RequestHandler } from 'express' import { ensureError, type Optional, throwUncoveredError } from '@speckle/shared' -import { get, isArray, isObjectLike, isString } from 'lodash' +import { get, isArray, isObjectLike, isString } from 'lodash-es' import type { PassportAuthenticateHandlerBuilder } from '@/modules/auth/domain/operations' import { ExpectedAuthFailure } from '@/modules/auth/domain/const' import type { ResolveAuthRedirectPath } from '@/modules/serverinvites/services/operations' diff --git a/packages/server/modules/auth/strategies.ts b/packages/server/modules/auth/strategies.ts index 43ed26e26..60ad4b6c6 100644 --- a/packages/server/modules/auth/strategies.ts +++ b/packages/server/modules/auth/strategies.ts @@ -80,4 +80,4 @@ const setupStrategiesFactory = return authStrategies } -export = setupStrategiesFactory +export default setupStrategiesFactory diff --git a/packages/server/modules/auth/strategies/azureAd.ts b/packages/server/modules/auth/strategies/azureAd.ts index 73c62aa88..2af3d2ab1 100644 --- a/packages/server/modules/auth/strategies/azureAd.ts +++ b/packages/server/modules/auth/strategies/azureAd.ts @@ -223,4 +223,4 @@ const azureAdStrategyBuilderFactory = } } -export = azureAdStrategyBuilderFactory +export default azureAdStrategyBuilderFactory diff --git a/packages/server/modules/auth/strategies/github.ts b/packages/server/modules/auth/strategies/github.ts index 6c120425c..6d89edc26 100644 --- a/packages/server/modules/auth/strategies/github.ts +++ b/packages/server/modules/auth/strategies/github.ts @@ -17,7 +17,7 @@ import { getServerOrigin } from '@/modules/shared/helpers/envHelper' import type { Request } from 'express' -import { get } from 'lodash' +import { get } from 'lodash-es' import { ensureError, Optional } from '@speckle/shared' import { ServerInviteRecord } from '@/modules/serverinvites/domain/types' import { @@ -200,4 +200,4 @@ const githubStrategyBuilderFactory = return strategy } -export = githubStrategyBuilderFactory +export default githubStrategyBuilderFactory diff --git a/packages/server/modules/auth/strategies/google.ts b/packages/server/modules/auth/strategies/google.ts index 5c052c05b..f4654a5f5 100644 --- a/packages/server/modules/auth/strategies/google.ts +++ b/packages/server/modules/auth/strategies/google.ts @@ -212,4 +212,4 @@ const googleStrategyBuilderFactory = return strategy } -export = googleStrategyBuilderFactory +export default googleStrategyBuilderFactory diff --git a/packages/server/modules/auth/strategies/local.ts b/packages/server/modules/auth/strategies/local.ts index 5a743dc7f..e5bdf8ca4 100644 --- a/packages/server/modules/auth/strategies/local.ts +++ b/packages/server/modules/auth/strategies/local.ts @@ -162,4 +162,4 @@ const localStrategyBuilderFactory = return strategy } -export = localStrategyBuilderFactory +export default localStrategyBuilderFactory diff --git a/packages/server/modules/auth/strategies/oidc.ts b/packages/server/modules/auth/strategies/oidc.ts index 19898f27b..1e72ba16f 100644 --- a/packages/server/modules/auth/strategies/oidc.ts +++ b/packages/server/modules/auth/strategies/oidc.ts @@ -16,7 +16,7 @@ import { getNameFromUserInfo } from '@/modules/auth/helpers/oidc' import { ServerInviteResourceType } from '@/modules/serverinvites/domain/constants' import { getResourceTypeRole } from '@/modules/serverinvites/helpers/core' import { AuthStrategyBuilder } from '@/modules/auth/helpers/types' -import { get } from 'lodash' +import { get } from 'lodash-es' import { ensureError, Optional } from '@speckle/shared' import { ServerInviteRecord } from '@/modules/serverinvites/domain/types' import { @@ -203,4 +203,4 @@ const oidcStrategyBuilderFactory = } } -export = oidcStrategyBuilderFactory +export default oidcStrategyBuilderFactory diff --git a/packages/server/modules/auth/tests/auth.spec.ts b/packages/server/modules/auth/tests/auth.spec.ts index 9c9553a72..2e2c7d8dc 100644 --- a/packages/server/modules/auth/tests/auth.spec.ts +++ b/packages/server/modules/auth/tests/auth.spec.ts @@ -62,7 +62,7 @@ import { RateLimiterMemory } from 'rate-limiter-flexible' import { TIME } from '@speckle/shared' import type { Application } from 'express' import { passportAuthenticationCallbackFactory } from '@/modules/auth/services/passportService' -import { testLogger as logger } from '@/observability/logging' +import { extendLoggerComponent, logger as baseLogger } from '@/observability/logging' import { processFinalizedProjectInviteFactory, validateProjectInviteBeforeFinalizationFactory @@ -188,6 +188,7 @@ const createUser = createUserFactory({ }) const getUserByEmail = legacyGetUserByEmailFactory({ db }) const updateServerInfo = updateServerInfoFactory({ db }) +const logger = extendLoggerComponent(baseLogger, 'auth-tests') const expect = chai.expect diff --git a/packages/server/modules/auth/tests/helpers/registration.ts b/packages/server/modules/auth/tests/helpers/registration.ts index 79c8e1234..77760e368 100644 --- a/packages/server/modules/auth/tests/helpers/registration.ts +++ b/packages/server/modules/auth/tests/helpers/registration.ts @@ -2,7 +2,7 @@ import { faker } from '@faker-js/faker' import { RelativeURL } from '@speckle/shared' import { expect } from 'chai' import type { Express } from 'express' -import { has, isString } from 'lodash' +import { has, isString } from 'lodash-es' import request from 'supertest' export const appId = 'spklwebapp' // same values as on FE diff --git a/packages/server/modules/auth/tests/integration/registration.spec.ts b/packages/server/modules/auth/tests/integration/registration.spec.ts index 349c25687..bff2cc4c0 100644 --- a/packages/server/modules/auth/tests/integration/registration.spec.ts +++ b/packages/server/modules/auth/tests/integration/registration.spec.ts @@ -25,7 +25,6 @@ import { TestApolloServer } from '@/test/graphqlHelper' import { beforeEachContext } from '@/test/hooks' -import { EmailSendingServiceMock } from '@/test/mocks/global' import { captureCreatedInvite } from '@/test/speckle-helpers/inviteHelper' import { BasicTestStream, @@ -89,10 +88,6 @@ describe('Server registration', () => { }) }) - afterEach(() => { - EmailSendingServiceMock.resetMockedFunctions() - }) - describe('with local strategy (email/pw)', () => { it('works', async () => { const challenge = 'asd123' diff --git a/packages/server/modules/automate/clients/executionEngine.ts b/packages/server/modules/automate/clients/executionEngine.ts index 6c71bd57f..70d1bbece 100644 --- a/packages/server/modules/automate/clients/executionEngine.ts +++ b/packages/server/modules/automate/clients/executionEngine.ts @@ -30,7 +30,7 @@ import { } from '@speckle/shared' import { randomUUID } from 'crypto' import { automateLogger, type Logger } from '@/observability/logging' -import { has, isObjectLike, isEmpty } from 'lodash' +import { has, isObjectLike, isEmpty } from 'lodash-es' import { getRequestLogger } from '@/observability/utils/requestContext' export type AuthCodePayloadWithOrigin = AuthCodePayload & { origin: string } diff --git a/packages/server/modules/automate/graph/mocks/automate.ts b/packages/server/modules/automate/graph/mocks/automate.ts index 89cfaa02e..5177aaeca 100644 --- a/packages/server/modules/automate/graph/mocks/automate.ts +++ b/packages/server/modules/automate/graph/mocks/automate.ts @@ -17,7 +17,7 @@ import { getFeatureFlags } from '@/modules/shared/helpers/envHelper' import { faker } from '@faker-js/faker' import { Automate, isNullOrUndefined, SourceAppNames } from '@speckle/shared' import dayjs from 'dayjs' -import { times } from 'lodash' +import { times } from 'lodash-es' const { FF_AUTOMATE_MODULE_ENABLED } = getFeatureFlags() diff --git a/packages/server/modules/automate/graph/resolvers/automate.ts b/packages/server/modules/automate/graph/resolvers/automate.ts index c8a65f055..7b5bd2263 100644 --- a/packages/server/modules/automate/graph/resolvers/automate.ts +++ b/packages/server/modules/automate/graph/resolvers/automate.ts @@ -94,7 +94,7 @@ import { getFunctionInputsForFrontendFactory } from '@/modules/automate/services/encryption' import { buildDecryptor } from '@/modules/shared/utils/libsodium' -import { keyBy } from 'lodash' +import * as _ from 'lodash-es' import { redactWriteOnlyInputData } from '@/modules/automate/utils/jsonSchemaRedactor' import { ProjectSubscriptions, @@ -142,7 +142,7 @@ const createAppToken = createAppTokenFactory({ storeUserServerAppToken: storeUserServerAppTokenFactory({ db }) }) -export = (FF_AUTOMATE_MODULE_ENABLED +export default (FF_AUTOMATE_MODULE_ENABLED ? { /** * If automate module is enabled @@ -443,7 +443,7 @@ export = (FF_AUTOMATE_MODULE_ENABLED const fns = await ctx.loaders .forRegion({ db: projectDb }) .automations.getRevisionFunctions.load(parent.id) - const fnsReleases = keyBy( + const fnsReleases = _.keyBy( ( await ctx.loaders .forRegion({ db: projectDb }) diff --git a/packages/server/modules/automate/index.ts b/packages/server/modules/automate/index.ts index 34c19caa1..8090de905 100644 --- a/packages/server/modules/automate/index.ts +++ b/packages/server/modules/automate/index.ts @@ -394,4 +394,4 @@ const automateModule: SpeckleModule = { } } -export = automateModule +export default automateModule diff --git a/packages/server/modules/automate/repositories/automations.ts b/packages/server/modules/automate/repositories/automations.ts index 045f064ca..4942167e1 100644 --- a/packages/server/modules/automate/repositories/automations.ts +++ b/packages/server/modules/automate/repositories/automations.ts @@ -83,7 +83,7 @@ import { import { Nullable, StreamRoles, isNullOrUndefined } from '@speckle/shared' import cryptoRandomString from 'crypto-random-string' import { Knex } from 'knex' -import _, { clamp, groupBy, keyBy, pick } from 'lodash' +import { clamp, groupBy, keyBy, pick } from 'lodash-es' import { SetOptional, SetRequired } from 'type-fest' const tables = { @@ -163,7 +163,7 @@ export const upsertAutomationFunctionRunFactory = await tables .automationFunctionRuns(deps.db) .insert( - _.pick(automationFunctionRun, AutomationFunctionRuns.withoutTablePrefix.cols) + pick(automationFunctionRun, AutomationFunctionRuns.withoutTablePrefix.cols) ) .onConflict(AutomationFunctionRuns.withoutTablePrefix.col.id) .merge([ @@ -185,7 +185,7 @@ export const upsertAutomationRunFactory = async (automationRun: InsertableAutomationRun) => { await tables .automationRuns(deps.db) - .insert(_.pick(automationRun, AutomationRuns.withoutTablePrefix.cols)) + .insert(pick(automationRun, AutomationRuns.withoutTablePrefix.cols)) .onConflict(AutomationRuns.withoutTablePrefix.col.id) .merge([ AutomationRuns.withoutTablePrefix.col.status, @@ -198,7 +198,7 @@ export const upsertAutomationRunFactory = .insert( automationRun.triggers.map((t) => ({ automationRunId: automationRun.id, - ..._.pick(t, AutomationRunTriggers.withoutTablePrefix.cols) + ...pick(t, AutomationRunTriggers.withoutTablePrefix.cols) })) ) .onConflict() @@ -207,7 +207,7 @@ export const upsertAutomationRunFactory = .automationFunctionRuns(deps.db) .insert( automationRun.functionRuns.map((f) => ({ - ..._.pick(f, AutomationFunctionRuns.withoutTablePrefix.cols), + ...pick(f, AutomationFunctionRuns.withoutTablePrefix.cols), runId: automationRun.id })) ) @@ -372,7 +372,7 @@ export const storeAutomationRevisionFactory = (deps: { db: Knex }): StoreAutomationRevision => async (revision: InsertableAutomationRevision) => { const id = revision.id || generateRevisionId() - const rev = _.pick(revision, AutomationRevisions.withoutTablePrefix.cols) + const rev = pick(revision, AutomationRevisions.withoutTablePrefix.cols) const [newRev] = await tables .automationRevisions(deps.db) .insert({ diff --git a/packages/server/modules/automate/services/authCode.ts b/packages/server/modules/automate/services/authCode.ts index 1240753bf..bdc5fb9ce 100644 --- a/packages/server/modules/automate/services/authCode.ts +++ b/packages/server/modules/automate/services/authCode.ts @@ -4,7 +4,7 @@ import { AutomateAuthCodeHandshakeError } from '@/modules/automate/errors/manage import { EventBus } from '@/modules/shared/services/eventBus' import cryptoRandomString from 'crypto-random-string' import Redis from 'ioredis' -import { get, has, isObjectLike } from 'lodash' +import { get, has, isObjectLike } from 'lodash-es' import { Logger } from 'pino' import { WorkspaceEvents } from '@/modules/workspacesCore/domain/events' diff --git a/packages/server/modules/automate/services/automationManagement.ts b/packages/server/modules/automate/services/automationManagement.ts index 4b8d761e9..a2c87804b 100644 --- a/packages/server/modules/automate/services/automationManagement.ts +++ b/packages/server/modules/automate/services/automationManagement.ts @@ -34,7 +34,7 @@ import { AutomationRunStatuses, VersionCreationTriggerType } from '@/modules/automate/helpers/types' -import { keyBy, uniq } from 'lodash' +import { keyBy, uniq } from 'lodash-es' import { resolveStatusFromFunctionRunStatuses } from '@/modules/automate/services/runsManagement' import { TriggeredAutomationsStatusGraphQLReturn } from '@/modules/automate/helpers/graphTypes' import { FunctionInputDecryptor } from '@/modules/automate/services/encryption' @@ -57,7 +57,7 @@ import { GetBranchesByIds } from '@/modules/core/domain/branches/operations' import { ValidateStreamAccess } from '@/modules/core/domain/streams/operations' import { EventBusEmit } from '@/modules/shared/services/eventBus' import { AutomationEvents } from '@/modules/automate/domain/events' -import { UnformattableTriggerDefinitionSchemaError } from '@speckle/shared/dist/commonjs/automate/index.js' +import { UnformattableTriggerDefinitionSchemaError } from '@speckle/shared/automate' export type CreateAutomationDeps = { createAuthCode: CreateStoredAuthCode diff --git a/packages/server/modules/automate/services/encryption.ts b/packages/server/modules/automate/services/encryption.ts index 8cfc98d66..cdb216a8f 100644 --- a/packages/server/modules/automate/services/encryption.ts +++ b/packages/server/modules/automate/services/encryption.ts @@ -2,7 +2,7 @@ import { getEncryptionKeysPath } from '@/modules/shared/helpers/envHelper' import { packageRoot } from '@/bootstrap' import path from 'node:path' import fs from 'node:fs/promises' -import { has, isArray, isObjectLike } from 'lodash' +import { has, isArray, isObjectLike } from 'lodash-es' import { Nullable, Optional } from '@speckle/shared' import { MisconfiguredEnvironmentError } from '@/modules/shared/errors' import { AutomationFunctionInputEncryptionError } from '@/modules/automate/errors/management' diff --git a/packages/server/modules/automate/services/functionManagement.ts b/packages/server/modules/automate/services/functionManagement.ts index 40ba3e02d..39dcace79 100644 --- a/packages/server/modules/automate/services/functionManagement.ts +++ b/packages/server/modules/automate/services/functionManagement.ts @@ -46,7 +46,7 @@ import { getFunctionsMarketplaceUrl } from '@/modules/core/helpers/routeHelper' import type { Logger } from '@/observability/logging' import { CreateStoredAuthCode } from '@/modules/automate/domain/operations' import { GetUser } from '@/modules/core/domain/users/operations' -import { noop } from 'lodash' +import { noop } from 'lodash-es' import { UnknownFunctionTemplateError } from '@/modules/automate/errors/functions' import { UserInputError } from '@/modules/core/errors/userinput' diff --git a/packages/server/modules/automate/tests/automations.spec.ts b/packages/server/modules/automate/tests/automations.spec.ts index 6dd4e364d..6bc58bde6 100644 --- a/packages/server/modules/automate/tests/automations.spec.ts +++ b/packages/server/modules/automate/tests/automations.spec.ts @@ -40,7 +40,7 @@ import { import { BasicTestStream, createTestStreams } from '@/test/speckle-helpers/streamHelper' import { Automate, Roles } from '@speckle/shared' import { expect } from 'chai' -import { times } from 'lodash' +import { times } from 'lodash-es' import { getFeatureFlags } from '@/modules/shared/helpers/envHelper' import { db } from '@/db/knex' import { diff --git a/packages/server/modules/backgroundjobs/index.ts b/packages/server/modules/backgroundjobs/index.ts index 0e3bc74e1..14a9271dd 100644 --- a/packages/server/modules/backgroundjobs/index.ts +++ b/packages/server/modules/backgroundjobs/index.ts @@ -7,4 +7,4 @@ const backgroundJobsModule: SpeckleModule = { } } -export = backgroundJobsModule +export default backgroundJobsModule diff --git a/packages/server/modules/blobstorage/clients/objectStorage.ts b/packages/server/modules/blobstorage/clients/objectStorage.ts index 119034cd0..437af161d 100644 --- a/packages/server/modules/blobstorage/clients/objectStorage.ts +++ b/packages/server/modules/blobstorage/clients/objectStorage.ts @@ -19,22 +19,26 @@ import { GetSignedUrl } from '@/modules/blobstorage/domain/operations' -export type ObjectStorage = { - client: S3Client - bucket: string -} - export type GetProjectObjectStorage = (args: { projectId: string }) => Promise export type GetObjectStorageParams = { - credentials: S3ClientConfig['credentials'] - endpoint: S3ClientConfig['endpoint'] - region: S3ClientConfig['region'] + credentials: { + accessKeyId: string + secretAccessKey: string + } + endpoint: string + region: string bucket: string } +export type ObjectStorage = { + client: S3Client + bucket: string + params: GetObjectStorageParams +} + /** * Get object storage client */ @@ -48,7 +52,7 @@ export const getObjectStorage = (params: GetObjectStorageParams): ObjectStorage forcePathStyle: true } const client = new S3Client(config) - return { client, bucket } + return { client, bucket, params } } let mainObjectStorage: Optional = undefined diff --git a/packages/server/modules/blobstorage/graph/resolvers/index.ts b/packages/server/modules/blobstorage/graph/resolvers/index.ts index 23faf86d5..16625e2f1 100644 --- a/packages/server/modules/blobstorage/graph/resolvers/index.ts +++ b/packages/server/modules/blobstorage/graph/resolvers/index.ts @@ -65,7 +65,7 @@ const streamBlobResolvers = { } } -export = { +export default { ServerInfo: { //deprecated blobSizeLimitBytes() { diff --git a/packages/server/modules/blobstorage/migrations/202206030936_add_asset_storage.js b/packages/server/modules/blobstorage/migrations/202206030936_add_asset_storage.js index e9fdfb7e1..6230ae6f1 100644 --- a/packages/server/modules/blobstorage/migrations/202206030936_add_asset_storage.js +++ b/packages/server/modules/blobstorage/migrations/202206030936_add_asset_storage.js @@ -6,7 +6,7 @@ const TABLE_NAME = 'blob_storage' * @param { import("knex").Knex } knex * @returns { Promise } */ -exports.up = async (knex) => { +const up = async (knex) => { await knex.schema.createTable(TABLE_NAME, (table) => { table.string('id', 10) // dont cascade on delete, cause it doesn't clean the object storage for the objs @@ -26,6 +26,8 @@ exports.up = async (knex) => { }) } -exports.down = async (knex) => { +const down = async (knex) => { await knex.schema.dropTableIfExists(TABLE_NAME) } + +export { up, down } diff --git a/packages/server/modules/blobstorage/migrations/202206231429_add_file_hash_to_blobs.js b/packages/server/modules/blobstorage/migrations/202206231429_add_file_hash_to_blobs.js index df814c8ea..6ae92f17d 100644 --- a/packages/server/modules/blobstorage/migrations/202206231429_add_file_hash_to_blobs.js +++ b/packages/server/modules/blobstorage/migrations/202206231429_add_file_hash_to_blobs.js @@ -7,14 +7,16 @@ const HASH_COLUMN_NAME = 'fileHash' * @param { import("knex").Knex } knex * @returns { Promise } */ -exports.up = async (knex) => { +const up = async (knex) => { await knex.schema.table(TABLE_NAME, (table) => { table.string(HASH_COLUMN_NAME) }) } -exports.down = async (knex) => { +const down = async (knex) => { await knex.schema.alterTable(TABLE_NAME, (table) => { table.dropColumn(HASH_COLUMN_NAME) }) } + +export { up, down } diff --git a/packages/server/modules/blobstorage/migrations/20220727091536_blobs-id-length-removal.js b/packages/server/modules/blobstorage/migrations/20220727091536_blobs-id-length-removal.js index 579bebb43..ec02288fe 100644 --- a/packages/server/modules/blobstorage/migrations/20220727091536_blobs-id-length-removal.js +++ b/packages/server/modules/blobstorage/migrations/20220727091536_blobs-id-length-removal.js @@ -2,7 +2,7 @@ * @param { import("knex").Knex } knex * @returns { Promise } */ -exports.up = async (knex) => { +const up = async (knex) => { await knex.raw( 'ALTER TABLE "blob_storage" ALTER COLUMN "id" SET DATA TYPE varchar(255);' ) @@ -12,8 +12,10 @@ exports.up = async (knex) => { * @param { import("knex").Knex } knex * @returns { Promise } */ -exports.down = async (knex) => { +const down = async (knex) => { await knex.raw( 'ALTER TABLE "blob_storage" ALTER COLUMN "id" SET DATA TYPE varchar(10);' ) } + +export { up, down } diff --git a/packages/server/modules/blobstorage/repositories/blobs.ts b/packages/server/modules/blobstorage/repositories/blobs.ts index 475403e1c..960025920 100644 --- a/packages/server/modules/blobstorage/repositories/blobs.ts +++ b/packages/server/modules/blobstorage/repositories/blobs.ts @@ -22,7 +22,7 @@ import { import { Upload } from '@aws-sdk/lib-storage' import type { Command } from '@aws-sdk/smithy-client' import { ensureError } from '@speckle/shared' -import { get } from 'lodash' +import { get } from 'lodash-es' import type stream from 'stream' const sendCommand = async ( diff --git a/packages/server/modules/blobstorage/rest/router.ts b/packages/server/modules/blobstorage/rest/router.ts index de1214d99..2f49133e8 100644 --- a/packages/server/modules/blobstorage/rest/router.ts +++ b/packages/server/modules/blobstorage/rest/router.ts @@ -6,7 +6,7 @@ import { streamReadPermissionsPipelineFactory } from '@/modules/shared/authz' import { authMiddlewareCreator } from '@/modules/shared/middleware' -import { isArray } from 'lodash' +import { isArray } from 'lodash-es' import { UnauthorizedError } from '@/modules/shared/errors' import { getAllStreamBlobIdsFactory, diff --git a/packages/server/modules/blobstorage/services/presigned.ts b/packages/server/modules/blobstorage/services/presigned.ts index 64b83b50c..b350c6882 100644 --- a/packages/server/modules/blobstorage/services/presigned.ts +++ b/packages/server/modules/blobstorage/services/presigned.ts @@ -16,7 +16,7 @@ import { AlreadyRegisteredBlobError, StoredBlobAccessError } from '@/modules/blobstorage/errors' -import { isEmpty } from 'lodash' +import { isEmpty } from 'lodash-es' import { MisconfiguredEnvironmentError } from '@/modules/shared/errors' // import { acceptedFileExtensions } from '@speckle/shared' diff --git a/packages/server/modules/blobstorage/services/streams.ts b/packages/server/modules/blobstorage/services/streams.ts index 6c886f215..45e1223de 100644 --- a/packages/server/modules/blobstorage/services/streams.ts +++ b/packages/server/modules/blobstorage/services/streams.ts @@ -20,7 +20,7 @@ import { getProjectObjectStorage } from '@/modules/multiregion/utils/blobStorage import { getProjectDbClient } from '@/modules/multiregion/utils/dbSelector' import type { Logger } from '@/observability/logging' import type { Readable, Writable } from 'stream' -import { get } from 'lodash' +import { get } from 'lodash-es' import type { UploadResult, ProcessingResult } from '@/modules/blobstorage/domain/types' import type { Busboy } from 'busboy' diff --git a/packages/server/modules/blobstorage/tests/e2e/blobstorage.rest.spec.ts b/packages/server/modules/blobstorage/tests/e2e/blobstorage.rest.spec.ts index f9125d6f3..c821b87ef 100644 --- a/packages/server/modules/blobstorage/tests/e2e/blobstorage.rest.spec.ts +++ b/packages/server/modules/blobstorage/tests/e2e/blobstorage.rest.spec.ts @@ -42,6 +42,7 @@ import { BasicTestUser } from '@/test/authHelper' import cryptoRandomString from 'crypto-random-string' import type { BlobStorageItem } from '@/modules/blobstorage/domain/types' import { getEventBus } from '@/modules/shared/services/eventBus' +import { fileURLToPath } from 'url' const getServerInfo = getServerInfoFactory({ db }) @@ -134,8 +135,8 @@ describe('Blobs integration @blobstorage', () => { const response = await request(app) .post(`/api/stream/${streamId}/blob`) .set('Authorization', `Bearer ${token}`) - .attach('blob1', require.resolve('@/readme.md')) - .attach('blob2', require.resolve('@/package.json')) + .attach('blob1', fileURLToPath(import.meta.resolve('@/readme.md'))) + .attach('blob2', fileURLToPath(import.meta.resolve('@/package.json'))) expect(response.status).to.equal(201) expect(response.body.uploadResults).to.exist const uploadResults = response.body.uploadResults diff --git a/packages/server/modules/blobstorage/tests/helpers.ts b/packages/server/modules/blobstorage/tests/helpers.ts index cb18cf7ca..043a784cc 100644 --- a/packages/server/modules/blobstorage/tests/helpers.ts +++ b/packages/server/modules/blobstorage/tests/helpers.ts @@ -1,6 +1,6 @@ /* istanbul ignore file */ import crs from 'crypto-random-string' -import { range } from 'lodash' +import { range } from 'lodash-es' import { knex } from '@/db/knex' const BlobStorage = () => knex('blob_storage') diff --git a/packages/server/modules/blobstorage/tests/integration/blobstorage.integration.spec.ts b/packages/server/modules/blobstorage/tests/integration/blobstorage.integration.spec.ts index 380e54238..68c390417 100644 --- a/packages/server/modules/blobstorage/tests/integration/blobstorage.integration.spec.ts +++ b/packages/server/modules/blobstorage/tests/integration/blobstorage.integration.spec.ts @@ -1,6 +1,6 @@ import { beforeEachContext } from '@/test/hooks' import { NotFoundError, BadRequestError } from '@/modules/shared/errors' -import { range } from 'lodash' +import { range } from 'lodash-es' import { fakeIdGenerator, createBlobs } from '@/modules/blobstorage/tests/helpers' import { uploadFileStreamFactory, diff --git a/packages/server/modules/blobstorage/tests/integration/presigned.integration.spec.ts b/packages/server/modules/blobstorage/tests/integration/presigned.integration.spec.ts index ad19720cd..7856d5d8b 100644 --- a/packages/server/modules/blobstorage/tests/integration/presigned.integration.spec.ts +++ b/packages/server/modules/blobstorage/tests/integration/presigned.integration.spec.ts @@ -25,7 +25,7 @@ import { Knex } from 'knex' import cryptoRandomString from 'crypto-random-string' import { expect } from 'chai' import { testLogger } from '@/observability/logging' -import { put } from 'axios' +import axios from 'axios' import { expectToThrow } from '@/test/assertionHelper' import { AlreadyRegisteredBlobError, @@ -148,7 +148,7 @@ const { FF_LARGE_FILE_IMPORTS_ENABLED } = getFeatureFlags() const fileSize = 100 - const response = await put(url, cryptoRandomString({ length: fileSize })) + const response = await axios.put(url, cryptoRandomString({ length: fileSize })) expect( response.status, JSON.stringify({ statusText: response.statusText, body: response.data }) @@ -205,7 +205,7 @@ const { FF_LARGE_FILE_IMPORTS_ENABLED } = getFeatureFlags() urlExpiryDurationSeconds: expiryDuration }) - const response = await put(url, 'test content') // more than 1 byte long + const response = await axios.put(url, 'test content') // more than 1 byte long expect( response.status, JSON.stringify({ statusText: response.statusText, body: response.data }) @@ -245,7 +245,7 @@ const { FF_LARGE_FILE_IMPORTS_ENABLED } = getFeatureFlags() urlExpiryDurationSeconds: expiryDuration }) - const response = await put(url, 'test content') + const response = await axios.put(url, 'test content') expect( response.status, JSON.stringify({ statusText: response.statusText, body: response.data }) @@ -291,7 +291,7 @@ const { FF_LARGE_FILE_IMPORTS_ENABLED } = getFeatureFlags() urlExpiryDurationSeconds: expiryDuration }) - const response = await put(url, cryptoRandomString({ length: 100 })) // more than 1 byte long + const response = await axios.put(url, cryptoRandomString({ length: 100 })) // more than 1 byte long expect( response.status, JSON.stringify({ statusText: response.statusText, body: response.data }) @@ -337,7 +337,7 @@ const { FF_LARGE_FILE_IMPORTS_ENABLED } = getFeatureFlags() const fileSize = 100 - const response = await put(url, cryptoRandomString({ length: fileSize })) + const response = await axios.put(url, cryptoRandomString({ length: fileSize })) expect( response.status, JSON.stringify({ statusText: response.statusText, body: response.data }) diff --git a/packages/server/modules/cli/commands/activities.ts b/packages/server/modules/cli/commands/activities.ts index d6275f722..d88fa714d 100644 --- a/packages/server/modules/cli/commands/activities.ts +++ b/packages/server/modules/cli/commands/activities.ts @@ -1,4 +1,4 @@ -import { noop } from 'lodash' +import { noop } from 'lodash-es' import { CommandModule } from 'yargs' const command: CommandModule = { diff --git a/packages/server/modules/cli/commands/bull.ts b/packages/server/modules/cli/commands/bull.ts index 2b8b39a63..f34ba281e 100644 --- a/packages/server/modules/cli/commands/bull.ts +++ b/packages/server/modules/cli/commands/bull.ts @@ -1,4 +1,4 @@ -import { noop } from 'lodash' +import { noop } from 'lodash-es' import { CommandModule } from 'yargs' const command: CommandModule = { diff --git a/packages/server/modules/cli/commands/bull/monitor.ts b/packages/server/modules/cli/commands/bull/monitor.ts index e4315cb79..c125e4e06 100644 --- a/packages/server/modules/cli/commands/bull/monitor.ts +++ b/packages/server/modules/cli/commands/bull/monitor.ts @@ -7,7 +7,7 @@ import { NOTIFICATIONS_QUEUE, buildNotificationsQueue } from '@/modules/notifications/services/queue' -import { noop } from 'lodash' +import { noop } from 'lodash-es' import { cliLogger } from '@/observability/logging' const PORT = 3032 diff --git a/packages/server/modules/cli/commands/bull/test-consume.ts b/packages/server/modules/cli/commands/bull/test-consume.ts index 867b41033..ef5bcc46f 100644 --- a/packages/server/modules/cli/commands/bull/test-consume.ts +++ b/packages/server/modules/cli/commands/bull/test-consume.ts @@ -2,7 +2,7 @@ import { cliLogger } from '@/observability/logging' import { NotificationType } from '@/modules/notifications/helpers/types' import { initializeConsumption } from '@/modules/notifications/index' import { EnvironmentResourceError } from '@/modules/shared/errors' -import { get, noop } from 'lodash' +import { get, noop } from 'lodash-es' import { CommandModule } from 'yargs' const command: CommandModule = { diff --git a/packages/server/modules/cli/commands/db.ts b/packages/server/modules/cli/commands/db.ts index b841cd7d5..e1855f5a4 100644 --- a/packages/server/modules/cli/commands/db.ts +++ b/packages/server/modules/cli/commands/db.ts @@ -1,4 +1,4 @@ -import { noop } from 'lodash' +import { noop } from 'lodash-es' import { CommandModule } from 'yargs' const command: CommandModule = { diff --git a/packages/server/modules/cli/commands/db/migrate.ts b/packages/server/modules/cli/commands/db/migrate.ts index 6d588d1ef..5a3598135 100644 --- a/packages/server/modules/cli/commands/db/migrate.ts +++ b/packages/server/modules/cli/commands/db/migrate.ts @@ -1,4 +1,4 @@ -import { noop } from 'lodash' +import { noop } from 'lodash-es' import { CommandModule } from 'yargs' const command: CommandModule = { diff --git a/packages/server/modules/cli/commands/db/seed.ts b/packages/server/modules/cli/commands/db/seed.ts index c7e73d874..661857e40 100644 --- a/packages/server/modules/cli/commands/db/seed.ts +++ b/packages/server/modules/cli/commands/db/seed.ts @@ -1,4 +1,4 @@ -import { noop } from 'lodash' +import { noop } from 'lodash-es' import { CommandModule } from 'yargs' const command: CommandModule = { diff --git a/packages/server/modules/cli/commands/db/seed/commits.ts b/packages/server/modules/cli/commands/db/seed/commits.ts index bab107e40..cd26b8bd9 100644 --- a/packages/server/modules/cli/commands/db/seed/commits.ts +++ b/packages/server/modules/cli/commands/db/seed/commits.ts @@ -7,7 +7,7 @@ import { getUserFactory } from '@/modules/core/repositories/users' import { ForbiddenError } from '@/modules/shared/errors' import { BasicTestCommit, createTestCommits } from '@/test/speckle-helpers/commitHelper' import dayjs from 'dayjs' -import { times } from 'lodash' +import { times } from 'lodash-es' import { CommandModule } from 'yargs' import { ProjectRecordVisibility } from '@/modules/core/helpers/types' diff --git a/packages/server/modules/cli/commands/db/seed/users.ts b/packages/server/modules/cli/commands/db/seed/users.ts index 023bbed16..966887e26 100644 --- a/packages/server/modules/cli/commands/db/seed/users.ts +++ b/packages/server/modules/cli/commands/db/seed/users.ts @@ -2,7 +2,7 @@ import { cliLogger as logger } from '@/observability/logging' import { Users, ServerAcl } from '@/modules/core/dbSchema' import { Roles } from '@/modules/core/helpers/mainConstants' import { faker } from '@faker-js/faker' -import { range } from 'lodash' +import { range } from 'lodash-es' import { UniqueEnforcer } from 'enforce-unique' import { CommandModule } from 'yargs' import { UserRecord } from '@/modules/core/helpers/types' diff --git a/packages/server/modules/cli/commands/download.ts b/packages/server/modules/cli/commands/download.ts index 88c468286..dc540d049 100644 --- a/packages/server/modules/cli/commands/download.ts +++ b/packages/server/modules/cli/commands/download.ts @@ -1,4 +1,4 @@ -import { noop } from 'lodash' +import { noop } from 'lodash-es' import { CommandModule } from 'yargs' const command: CommandModule = { diff --git a/packages/server/modules/cli/commands/graphql.ts b/packages/server/modules/cli/commands/graphql.ts index 564a38469..85924b5e6 100644 --- a/packages/server/modules/cli/commands/graphql.ts +++ b/packages/server/modules/cli/commands/graphql.ts @@ -1,4 +1,4 @@ -import { noop } from 'lodash' +import { noop } from 'lodash-es' import { CommandModule } from 'yargs' const command: CommandModule = { diff --git a/packages/server/modules/cli/commands/graphql/introspect.ts b/packages/server/modules/cli/commands/graphql/introspect.ts index f3bdaee1a..e12189e66 100644 --- a/packages/server/modules/cli/commands/graphql/introspect.ts +++ b/packages/server/modules/cli/commands/graphql/introspect.ts @@ -18,7 +18,7 @@ const command: CommandModule = { }, handler: async ({ file }) => { logger.info('Loading GQL schema...') - const schema = ModulesSetup.graphSchema() + const schema = await ModulesSetup.graphSchema() const schemaString = printSchema(schema) logger.info(`Saving to "${file}"...`) diff --git a/packages/server/modules/cli/commands/stream.ts b/packages/server/modules/cli/commands/stream.ts index 91a818005..285c2ccc2 100644 --- a/packages/server/modules/cli/commands/stream.ts +++ b/packages/server/modules/cli/commands/stream.ts @@ -1,4 +1,4 @@ -import { noop } from 'lodash' +import { noop } from 'lodash-es' import { CommandModule } from 'yargs' const command: CommandModule = { diff --git a/packages/server/modules/cli/commands/test.ts b/packages/server/modules/cli/commands/test.ts index a1423adf5..daa91e25c 100644 --- a/packages/server/modules/cli/commands/test.ts +++ b/packages/server/modules/cli/commands/test.ts @@ -1,4 +1,4 @@ -import { noop } from 'lodash' +import { noop } from 'lodash-es' import { CommandModule } from 'yargs' const command: CommandModule = { diff --git a/packages/server/modules/cli/commands/workspaces.ts b/packages/server/modules/cli/commands/workspaces.ts index 9213712e4..168ce0939 100644 --- a/packages/server/modules/cli/commands/workspaces.ts +++ b/packages/server/modules/cli/commands/workspaces.ts @@ -1,4 +1,4 @@ -import { noop } from 'lodash' +import { noop } from 'lodash-es' import { CommandModule } from 'yargs' const command: CommandModule = { diff --git a/packages/server/modules/cli/index.ts b/packages/server/modules/cli/index.ts index 7447681cc..b317c5069 100644 --- a/packages/server/modules/cli/index.ts +++ b/packages/server/modules/cli/index.ts @@ -1,16 +1,20 @@ /* eslint-disable no-restricted-imports */ +import '../../bootstrap.js' import path from 'path' import yargs from 'yargs' -import '../../bootstrap' +import { hideBin } from 'yargs/helpers' import { cliLogger as logger } from '@/observability/logging' import { isTestEnv } from '@/modules/shared/helpers/envHelper' -import { mochaHooks } from '@/test/hooks' +import { beforeEntireTestRun } from '@/test/hooks' +import { getModuleDirectory } from '@speckle/shared/environment/node' const main = async () => { - const execution = yargs + await yargs(hideBin(process.argv)) .scriptName('yarn cli') .usage('$0 [args]') - .commandDir(path.resolve(__dirname, './commands'), { extensions: ['js', 'ts'] }) + .commandDir(path.resolve(getModuleDirectory(import.meta), './commands'), { + extensions: ['js', 'ts'] + }) .option('beforeAll', { type: 'boolean', default: false, @@ -24,7 +28,7 @@ const main = async () => { // In test env, run beforeAll hooks to properly initialize everything first if (isBeforeAllSet && isTestEnv()) { logger.info('Running test beforeAll hooks...') - await (mochaHooks.beforeAll as () => Promise)() + await beforeEntireTestRun() } }) .fail((msg, err, yargs) => { @@ -40,12 +44,10 @@ const main = async () => { process.exit(1) }) - .help().argv + .help() + .parseAsync() - return execution + process.exit(0) } -void main().then(() => { - // weird TS typing issue - yargs.exit(0, undefined as unknown as Error) -}) +await main() diff --git a/packages/server/modules/comments/graph/dataloaders/index.ts b/packages/server/modules/comments/graph/dataloaders/index.ts index 53f2607c2..f3b4323e2 100644 --- a/packages/server/modules/comments/graph/dataloaders/index.ts +++ b/packages/server/modules/comments/graph/dataloaders/index.ts @@ -1,5 +1,5 @@ import { defineRequestDataloaders } from '@/modules/shared/helpers/graphqlHelper' -import { keyBy } from 'lodash' +import { keyBy } from 'lodash-es' import { Nullable } from '@/modules/shared/helpers/typeHelper' import { ResourceIdentifier } from '@/modules/core/graph/generated/graphql' import { diff --git a/packages/server/modules/comments/graph/resolvers/comments.ts b/packages/server/modules/comments/graph/resolvers/comments.ts index 04df4cc53..7a5880f03 100644 --- a/packages/server/modules/comments/graph/resolvers/comments.ts +++ b/packages/server/modules/comments/graph/resolvers/comments.ts @@ -32,7 +32,7 @@ import { ensureCommentSchema, validateInputAttachmentsFactory } from '@/modules/comments/services/commentTextService' -import { has } from 'lodash' +import { has } from 'lodash-es' import { documentToBasicString, SmartTextEditorValueSchema @@ -143,7 +143,7 @@ const getAuthorizedStreamCommentFactory = return comment } -export = { +export default { Query: { async comment(_parent, args, context) { const projectId = args.streamId diff --git a/packages/server/modules/comments/index.ts b/packages/server/modules/comments/index.ts index 7daac4f02..edc672c27 100644 --- a/packages/server/modules/comments/index.ts +++ b/packages/server/modules/comments/index.ts @@ -60,4 +60,4 @@ const commentsModule: SpeckleModule = { } } -export = commentsModule +export default commentsModule diff --git a/packages/server/modules/comments/migrations/20220222173000_comments.js b/packages/server/modules/comments/migrations/20220222173000_comments.js index 7e0a07efb..b67ab6c27 100644 --- a/packages/server/modules/comments/migrations/20220222173000_comments.js +++ b/packages/server/modules/comments/migrations/20220222173000_comments.js @@ -1,5 +1,5 @@ // /* istanbul ignore file */ -exports.up = async (knex) => { +const up = async (knex) => { await knex.schema.createTable('comments', (table) => { table.string('id', 10).primary() table @@ -53,8 +53,10 @@ exports.up = async (knex) => { }) } -exports.down = async (knex) => { +const down = async (knex) => { await knex.schema.dropTableIfExists('comment_views') await knex.schema.dropTableIfExists('comment_links') await knex.schema.dropTableIfExists('comments') } + +export { up, down } diff --git a/packages/server/modules/comments/migrations/20220722110643_fix_comments_delete_cascade.js b/packages/server/modules/comments/migrations/20220722110643_fix_comments_delete_cascade.js index 0e06a9ad0..9702dc998 100644 --- a/packages/server/modules/comments/migrations/20220722110643_fix_comments_delete_cascade.js +++ b/packages/server/modules/comments/migrations/20220722110643_fix_comments_delete_cascade.js @@ -1,4 +1,4 @@ -const { Users } = require('@/modules/core/dbSchema') +import { Users } from '@/modules/core/dbSchema' const COMMENTS_TABLE = 'comments' const COMMENT_VIEWS_TABLE = 'comment_views' @@ -7,7 +7,7 @@ const COMMENT_VIEWS_TABLE = 'comment_views' * @param { import("knex").Knex } knex * @returns { Promise } */ -exports.up = async function (knex) { +async function up(knex) { // Delete all orphaned comments, which can be there even though there was a FK there before for some reason await knex .table(COMMENTS_TABLE) @@ -41,7 +41,7 @@ exports.up = async function (knex) { * @param { import("knex").Knex } knex * @returns { Promise } */ -exports.down = async function (knex) { +async function down(knex) { await knex.schema.alterTable(COMMENTS_TABLE, (table) => { table.dropForeign('authorId') table.foreign('authorId').references(Users.col.id).onDelete('NO ACTION') @@ -58,3 +58,5 @@ exports.down = async function (knex) { table.foreign('userId').references(Users.col.id).onDelete('NO ACTION') }) } + +export { up, down } diff --git a/packages/server/modules/comments/repositories/comments.ts b/packages/server/modules/comments/repositories/comments.ts index 596e4ac06..bd8c7e5b9 100644 --- a/packages/server/modules/comments/repositories/comments.ts +++ b/packages/server/modules/comments/repositories/comments.ts @@ -20,7 +20,7 @@ import { ResourceType } from '@/modules/core/graph/generated/graphql' import { Optional } from '@/modules/shared/helpers/typeHelper' -import { clamp, keyBy, reduce } from 'lodash' +import { clamp, keyBy, reduce } from 'lodash-es' import crs from 'crypto-random-string' import { executeBatchedSelect } from '@/modules/shared/helpers/dbHelper' import { Knex } from 'knex' diff --git a/packages/server/modules/comments/services/commentTextService.ts b/packages/server/modules/comments/services/commentTextService.ts index b7e174d5d..09fb23153 100644 --- a/packages/server/modules/comments/services/commentTextService.ts +++ b/packages/server/modules/comments/services/commentTextService.ts @@ -8,7 +8,7 @@ import { isDocEmpty, documentToBasicString } from '@/modules/core/services/richTextEditorService' -import { isString, uniq } from 'lodash' +import { isString, uniq } from 'lodash-es' import { InvalidAttachmentsError } from '@/modules/comments/errors' import { JSONContent } from '@tiptap/core' import { ValidateInputAttachments } from '@/modules/comments/domain/operations' diff --git a/packages/server/modules/comments/services/data.ts b/packages/server/modules/comments/services/data.ts index 708570815..e831c9698 100644 --- a/packages/server/modules/comments/services/data.ts +++ b/packages/server/modules/comments/services/data.ts @@ -5,7 +5,7 @@ import { import { LegacyCommentViewerData } from '@/modules/core/graph/generated/graphql' import { viewerResourcesToString } from '@/modules/core/services/commit/viewerResources' import { Nullable, SpeckleViewer } from '@speckle/shared' -import { has, get, intersection, isObjectLike } from 'lodash' +import { has, get, intersection, isObjectLike } from 'lodash-es' type SerializedViewerState = SpeckleViewer.ViewerState.SerializedViewerState diff --git a/packages/server/modules/comments/services/index.ts b/packages/server/modules/comments/services/index.ts index 43bc6dc6b..db8845b10 100644 --- a/packages/server/modules/comments/services/index.ts +++ b/packages/server/modules/comments/services/index.ts @@ -62,7 +62,15 @@ export const createCommentFactory = emitEvent: EventBusEmit getViewerResourcesFromLegacyIdentifiers: GetViewerResourcesFromLegacyIdentifiers }) => - async ({ userId, input }: { userId: string; input: CommentCreateInput }) => { + async ( + { userId, input }: { userId: string; input: CommentCreateInput }, + options?: Partial<{ + /** + * Used in tests to skip text validation & formatting - text is saved in DB as is + */ + skipTextValidation: boolean + }> + ) => { if (input.resources.length < 1) throw new UserInputError( 'Must specify at least one resource as the comment target' @@ -91,10 +99,12 @@ export const createCommentFactory = } await deps.validateInputAttachments(input.streamId, input.blobIds) - comment.text = buildCommentTextFromInput({ - doc: input.text, - blobIds: input.blobIds - }) + comment.text = options?.skipTextValidation + ? (input.text as SmartTextEditorValueSchema) + : buildCommentTextFromInput({ + doc: input.text, + blobIds: input.blobIds + }) const id = crs({ length: 10 }) const [newComment] = await deps.insertComments([ diff --git a/packages/server/modules/comments/services/notifications.ts b/packages/server/modules/comments/services/notifications.ts index 4d2bf3f89..4a2636ffc 100644 --- a/packages/server/modules/comments/services/notifications.ts +++ b/packages/server/modules/comments/services/notifications.ts @@ -2,7 +2,7 @@ import { CommentRecord } from '@/modules/comments/helpers/types' import { ensureCommentSchema } from '@/modules/comments/services/commentTextService' import type { JSONContent } from '@tiptap/core' import { iterateContentNodes } from '@/modules/core/services/richTextEditorService' -import { difference, flatten } from 'lodash' +import { difference, flatten } from 'lodash-es' import { NotificationPublisher, NotificationType diff --git a/packages/server/modules/comments/services/retrieval.ts b/packages/server/modules/comments/services/retrieval.ts index 78c531eb9..d589af51c 100644 --- a/packages/server/modules/comments/services/retrieval.ts +++ b/packages/server/modules/comments/services/retrieval.ts @@ -1,5 +1,5 @@ import { Optional } from '@speckle/shared' -import { isUndefined } from 'lodash' +import { isUndefined } from 'lodash-es' import { GetPaginatedBranchCommentsFactory, GetPaginatedBranchCommentsPage, diff --git a/packages/server/modules/comments/tests/comments.spec.ts b/packages/server/modules/comments/tests/comments.spec.ts index 65724960a..c81216531 100644 --- a/packages/server/modules/comments/tests/comments.spec.ts +++ b/packages/server/modules/comments/tests/comments.spec.ts @@ -11,13 +11,16 @@ import { editCommentFactory, archiveCommentFactory } from '@/modules/comments/services/index' -import { convertBasicStringToDocument } from '@/modules/core/services/richTextEditorService' +import { + convertBasicStringToDocument, + SmartTextEditorValueSchema +} from '@/modules/core/services/richTextEditorService' import { ensureCommentSchema, buildCommentTextFromInput, validateInputAttachmentsFactory } from '@/modules/comments/services/commentTextService' -import { get, range } from 'lodash' +import { get, range } from 'lodash-es' import { buildApolloServer } from '@/app' import { AllScopes } from '@/modules/core/helpers/mainConstants' import { createAuthTokenForUser } from '@/test/authHelper' @@ -30,11 +33,6 @@ import { purgeNotifications } from '@/test/notificationsHelper' import { NotificationType } from '@/modules/notifications/helpers/types' -import { - EmailSendingServiceMock, - CommentsRepositoryMock, - StreamsRepositoryMock -} from '@/test/mocks/global' import { createAuthedTestContext, ServerAndContext } from '@/test/graphqlHelper' import { checkStreamResourceAccessFactory, @@ -128,7 +126,6 @@ import { LegacyCommentViewerData, ReplyCreateInput } from '@/modules/core/graph/generated/graphql' -import { CommentRecord } from '@/modules/comments/helpers/types' import { MaybeNullOrUndefined, TIME_MS } from '@speckle/shared' import { CommentEvents } from '@/modules/comments/domain/events' import { @@ -136,7 +133,6 @@ import { getViewerResourcesForCommentsFactory, getViewerResourcesFromLegacyIdentifiersFactory } from '@/modules/core/services/commit/viewerResources' -import { StreamRecord } from '@/modules/core/helpers/types' import { processFinalizedProjectInviteFactory, validateProjectInviteBeforeFinalizationFactory @@ -146,11 +142,9 @@ import { validateStreamAccessFactory } from '@/modules/core/services/streams/access' import { authorizeResolver } from '@/modules/shared' - -type LegacyCommentRecord = CommentRecord & { - total_count: string - resources: Array<{ resourceId: string; resourceType: string }> -} +import { createEmailListener, TestEmailListener } from '@/test/speckle-helpers/email' +import { buildTestProject } from '@/modules/core/tests/helpers/creation' +import { GetCommentsQueryVariables } from '@/test/graphql/generated/graphql' const getServerInfo = getServerInfoFactory({ db }) const getUser = getUserFactory({ db }) @@ -285,33 +279,34 @@ const buildFinalizeProjectInvite = () => getServerInfo }) -const createStream = legacyCreateStreamFactory({ - createStreamReturnRecord: createStreamReturnRecordFactory({ - inviteUsersToProject: inviteUsersToProjectFactory({ - createAndSendInvite: createAndSendInviteFactory({ - findUserByTarget: findUserByTargetFactory({ db }), - insertInviteAndDeleteOld: insertInviteAndDeleteOldFactory({ db }), - collectAndValidateResourceTargets: collectAndValidateCoreTargetsFactory({ - getStream - }), - buildInviteEmailContents: buildCoreInviteEmailContentsFactory({ - getStream - }), - emitEvent: ({ eventName, payload }) => - getEventBus().emit({ - eventName, - payload - }), - getUser, - getServerInfo, - finalizeInvite: buildFinalizeProjectInvite() +const createStreamReturnRecord = createStreamReturnRecordFactory({ + inviteUsersToProject: inviteUsersToProjectFactory({ + createAndSendInvite: createAndSendInviteFactory({ + findUserByTarget: findUserByTargetFactory({ db }), + insertInviteAndDeleteOld: insertInviteAndDeleteOldFactory({ db }), + collectAndValidateResourceTargets: collectAndValidateCoreTargetsFactory({ + getStream }), - getUsers + buildInviteEmailContents: buildCoreInviteEmailContentsFactory({ + getStream + }), + emitEvent: ({ eventName, payload }) => + getEventBus().emit({ + eventName, + payload + }), + getUser, + getServerInfo, + finalizeInvite: buildFinalizeProjectInvite() }), - createStream: createStreamFactory({ db }), - createBranch: createBranchFactory({ db }), - emitEvent: getEventBus().emit - }) + getUsers + }), + createStream: createStreamFactory({ db }), + createBranch: createBranchFactory({ db }), + emitEvent: getEventBus().emit +}) +const createStream = legacyCreateStreamFactory({ + createStreamReturnRecord }) const findEmail = findEmailFactory({ db }) @@ -353,9 +348,8 @@ function generateRandomCommentText() { return buildCommentInputFromString(crs({ length: 10 })) } -const mailerMock = EmailSendingServiceMock -const commentRepoMock = CommentsRepositoryMock -const streamsRepoMock = StreamsRepositoryMock +const buildTestStream = () => + buildTestProject({ workspaceId: undefined, regionKey: undefined }) describe('Comments @comments', () => { let app: express.Express @@ -434,13 +428,6 @@ describe('Comments @comments', () => { after(() => { notificationsState.destroy() - commentRepoMock.destroy() - streamsRepoMock.destroy() - }) - - afterEach(() => { - commentRepoMock.disable() - commentRepoMock.resetMockedFunctions() }) it('Should be able to create a comment and a reply', async () => { @@ -1498,7 +1485,7 @@ describe('Comments @comments', () => { ) }) - const createComment = (input = {}) => + const createCommentGql = (input = {}) => CommentsGraphQLClient.createComment(apollo, { input: { streamId: stream.id, @@ -1509,7 +1496,7 @@ describe('Comments @comments', () => { } }) - const createReply = (input?: ReplyCreateInput) => + const createReplyGql = (input?: ReplyCreateInput) => CommentsGraphQLClient.createReply(apollo, { input: { streamId: stream.id, @@ -1528,7 +1515,7 @@ describe('Comments @comments', () => { await truncateTables([Comments.name]) // Create a single comment with a blob - const createCommentResult = await createComment({ + const createCommentResult = await createCommentGql({ text: generateRandomCommentText(), blobIds: [blob1.blobId] }) @@ -1536,7 +1523,7 @@ describe('Comments @comments', () => { if (!parentCommentId) throw new Error('Comment creation failed!') // Create a reply with a blob - await createReply({ + await createReplyGql({ text: generateRandomCommentText(), blobIds: [blob1.blobId], parentComment: parentCommentId, @@ -1544,7 +1531,7 @@ describe('Comments @comments', () => { }) // Create a reply with a blob, but no text - const emptyCommentResult = await createReply({ + const emptyCommentResult = await createReplyGql({ blobIds: [blob1.blobId], parentComment: parentCommentId, streamId: stream.id @@ -1559,7 +1546,7 @@ describe('Comments @comments', () => { ...(input || { id: '' }) }) - const readComments = (input = {}) => + const readComments = (input: Partial = {}) => CommentsGraphQLClient.getComments(apollo, { cursor: null, streamId: stream.id, @@ -1567,61 +1554,99 @@ describe('Comments @comments', () => { }) it('both legacy (string) comments and new (ProseMirror) documents are formatted as SmartTextEditorValue values', async () => { - commentRepoMock.enable() - commentRepoMock.mockFunction('getCommentsLegacyFactory', () => { - return async () => ({ - items: [ - // Legacy - { - id: 'a', - text: 'hey dude! welcome to my legacy-type comment!', - streamId: stream.id - }, - // New - { - id: 'b', + const streamId = await createStream({ ...buildTestStream(), ownerId: user.id }) + + await Promise.all([ + // Legacy + createComment( + { + userId: user.id, + input: { + streamId, + resources: [ + { resourceId: streamId, resourceType: ResourceType.Stream } + ], + text: 'hey dude! welcome to my legacy-type comment!' as unknown as SmartTextEditorValueSchema, + data: {}, + blobIds: [] + } + }, + { skipTextValidation: true } + ), + // New + createComment( + { + userId: user.id, + input: { + streamId, + resources: [ + { resourceId: streamId, resourceType: ResourceType.Stream } + ], text: JSON.stringify( buildCommentTextFromInput({ doc: buildCommentInputFromString('new comment schema here') }) - ), - streamId: stream.id - }, - // New, but for some reason the text object is already deserialized - { - id: 'c', + ) as unknown as SmartTextEditorValueSchema, + data: {}, + blobIds: [] + } + }, + { skipTextValidation: true } + ), + // New, but for some reason the text object is already deserialized + createComment( + { + userId: user.id, + input: { + streamId, + resources: [ + { resourceId: streamId, resourceType: ResourceType.Stream } + ], text: buildCommentTextFromInput({ doc: buildCommentInputFromString('another new comment schema here') }), - streamId: stream.id + data: {}, + blobIds: [] } - ] as unknown as Array, - cursor: new Date().toISOString(), - totalCount: 3 - }) - }) + }, + { skipTextValidation: true } + ) + ]) - const { data, errors } = await readComments() + const { data, errors } = await readComments({ + streamId + }) expect(errors?.length || 0).to.eq(0) expect(data?.comments?.items?.length || 0).to.eq(3) }) it('legacy comment with a single link is formatted correctly', async () => { + const streamId = await createStream({ ...buildTestStream(), ownerId: user.id }) + + // Low-level insert cause all we need are just the main DB entries const item = { - id: '1', - text: 'https://aaa.com:3000/h3ll0-world/_?a=1&b=2#aaa', - streamId: stream.id - } as unknown as LegacyCommentRecord + text: 'https://aaa.com:3000/h3ll0-world/_?a=1&b=2#aaa' as unknown as SmartTextEditorValueSchema, + streamId, + authorId: user.id + } + await createComment( + { + userId: user.id, + input: { + streamId, + resources: [{ resourceId: streamId, resourceType: ResourceType.Stream }], + text: item.text, + data: {}, + blobIds: [] + } + }, + { skipTextValidation: true } + ) - commentRepoMock.enable() - commentRepoMock.mockFunction('getCommentsLegacyFactory', () => async () => ({ - items: [item], - cursor: new Date().toISOString(), - totalCount: 1 - })) - - const { data, errors } = await readComments() + const { data, errors } = await readComments({ + streamId + }) expect(data?.comments?.items?.length || 0).to.eq(1) expect(errors?.length || 0).to.eq(0) @@ -1637,6 +1662,8 @@ describe('Comments @comments', () => { }) it('legacy comment with multiple links formats them correctly', async () => { + const streamId = await createStream({ ...buildTestStream(), ownerId: user.id }) + const textParts = [ "Here's one ", // The period and comma def shouldn't belong to the following URL, but we have a pretty basic @@ -1648,20 +1675,26 @@ describe('Comments @comments', () => { 'http://agag.com:3000' ] + // Low-level insert cause all we need are just the main DB entries const item = { - id: '1', - text: textParts.join(''), - streamId: stream.id - } as unknown as LegacyCommentRecord - - commentRepoMock.enable() - commentRepoMock.mockFunction('getCommentsLegacyFactory', () => async () => ({ - items: [item], - cursor: new Date().toISOString(), - totalCount: 1 - })) - - const { data, errors } = await readComments() + text: textParts.join('') as unknown as SmartTextEditorValueSchema, + streamId, + authorId: user.id + } + await createComment( + { + userId: user.id, + input: { + streamId, + resources: [{ resourceId: streamId, resourceType: ResourceType.Stream }], + text: item.text, + data: {}, + blobIds: [] + } + }, + { skipTextValidation: true } + ) + const { data, errors } = await readComments({ streamId }) const runExpectationsOnTextNode = (idx: number, shouldBeLink: boolean) => { expect(textNodes[idx].text).to.eq(textParts[idx]) @@ -1724,7 +1757,7 @@ describe('Comments @comments', () => { }) it('returns raw text correctly', async () => { - const { data } = await createReply({ + const { data } = await createReplyGql({ text: { type: 'doc', content: [ @@ -1761,43 +1794,6 @@ describe('Comments @comments', () => { expect(data?.comment?.text?.doc).to.be.null expect(data?.comment?.text?.attachments?.length).to.be.greaterThan(0) }) - - const unexpectedValDataset = [ - { display: 'number', value: 3 }, - { display: 'random object', value: { a: 1, b: 2 } } - ] - unexpectedValDataset.forEach(({ display, value }) => { - it(`unexpected text value (${display}) in DB throw sanitized errors`, async () => { - streamsRepoMock.enable() - streamsRepoMock.mockFunction('getStreamsFactory', () => async () => [ - { - id: stream.id, - workspaceId: '' - } as unknown as StreamRecord - ]) - const item = { - id: '1', - text: value, - streamId: stream.id, - createdAt: new Date() - } as unknown as LegacyCommentRecord - - commentRepoMock.enable() - commentRepoMock.mockFunction('getCommentsLegacyFactory', () => async () => ({ - items: [item], - cursor: new Date().toISOString(), - totalCount: 1 - })) - - const { errors } = await readComments() - - expect((errors || []).map((e) => e.message).join(';')).to.contain( - 'Unexpected comment schema format' - ) - streamsRepoMock.disable() - streamsRepoMock.resetMockedFunctions() - }) - }) }) const creatingOrReplyingDataSet = [ @@ -1809,8 +1805,8 @@ describe('Comments @comments', () => { const createOrReplyComment = (input = {}) => creating - ? createComment(input) - : createReply({ + ? createCommentGql(input) + : createReplyGql({ parentComment: parentCommentId, blobIds: [], streamId: stream.id, @@ -1825,7 +1821,7 @@ describe('Comments @comments', () => { before(async () => { if (replying) { // Create comment for attaching replies to - const { data } = await createComment({ + const { data } = await createCommentGql({ text: generateRandomCommentText() }) @@ -1920,6 +1916,20 @@ describe('Comments @comments', () => { }) describe('and mentioning a user', () => { + let emailListener: TestEmailListener + + before(async () => { + emailListener = await createEmailListener() + }) + + after(async () => { + await emailListener.destroy() + }) + + afterEach(() => { + emailListener.reset() + }) + const createOrReplyCommentWithMention = (targetUserId: string, input = {}) => createOrReplyComment({ text: { @@ -1942,10 +1952,7 @@ describe('Comments @comments', () => { }) it('a valid mention triggers a notification', async () => { - const sendEmailInvocations = mailerMock.hijackFunction( - 'sendEmail', - async () => false - ) + const { getSends } = emailListener.listen({ times: 2 }) const waitForAck = notificationsState.waitForAck( (e) => e.result?.type === NotificationType.MentionedInComment @@ -1960,7 +1967,8 @@ describe('Comments @comments', () => { // Wait for await waitForAck - const emailParams = sendEmailInvocations.args[0][0] + const emailSends = getSends() + const emailParams = emailSends[0] expect(emailParams).to.be.ok expect(emailParams.subject).to.contain('mentioned in a Speckle comment') expect(emailParams.to).to.eq(otherUser.email) diff --git a/packages/server/modules/core/dbSchema.ts b/packages/server/modules/core/dbSchema.ts index d0f6528a8..2e47a5588 100644 --- a/packages/server/modules/core/dbSchema.ts +++ b/packages/server/modules/core/dbSchema.ts @@ -3,7 +3,7 @@ import { Optional } from '@speckle/shared' import knex from '@/db/knex' import { BaseMetaRecord } from '@/modules/core/helpers/meta' import { Knex } from 'knex' -import { reduce } from 'lodash' +import { reduce } from 'lodash-es' type BaseInnerSchemaConfig = { /** diff --git a/packages/server/modules/core/events/subscriptionListeners.ts b/packages/server/modules/core/events/subscriptionListeners.ts index a1e1afd37..c44d84b3a 100644 --- a/packages/server/modules/core/events/subscriptionListeners.ts +++ b/packages/server/modules/core/events/subscriptionListeners.ts @@ -24,7 +24,7 @@ import { UserSubscriptions, WorkspaceSubscriptions } from '@/modules/shared/utils/subscriptions' -import { chunk, flatten } from 'lodash' +import { chunk, flatten } from 'lodash-es' const reportModelCreatedFactory = (deps: { publish: PublishSubscription }) => diff --git a/packages/server/modules/core/graph/dataloaders/index.ts b/packages/server/modules/core/graph/dataloaders/index.ts index 90e7ec868..a480d9a9e 100644 --- a/packages/server/modules/core/graph/dataloaders/index.ts +++ b/packages/server/modules/core/graph/dataloaders/index.ts @@ -15,7 +15,7 @@ import { getStreamsCollaboratorsFactory, getStreamsCollaboratorCountsFactory } from '@/modules/core/repositories/streams' -import { keyBy } from 'lodash' +import { keyBy } from 'lodash-es' import { BranchRecord, CommitRecord, diff --git a/packages/server/modules/core/graph/resolvers/admin.ts b/packages/server/modules/core/graph/resolvers/admin.ts index ab885f32f..364339889 100644 --- a/packages/server/modules/core/graph/resolvers/admin.ts +++ b/packages/server/modules/core/graph/resolvers/admin.ts @@ -30,7 +30,7 @@ const adminProjectList = adminProjectListFactory({ getStreams: legacyGetStreamsFactory({ db }) }) -export = { +export default { Query: { admin: () => ({}) }, diff --git a/packages/server/modules/core/graph/resolvers/apitoken.ts b/packages/server/modules/core/graph/resolvers/apitoken.ts index cfcbb36af..3e612a8af 100644 --- a/packages/server/modules/core/graph/resolvers/apitoken.ts +++ b/packages/server/modules/core/graph/resolvers/apitoken.ts @@ -77,4 +77,4 @@ const resolvers = { } } as Resolvers -export = resolvers +export default resolvers diff --git a/packages/server/modules/core/graph/resolvers/appTokens.ts b/packages/server/modules/core/graph/resolvers/appTokens.ts index 6090f2d42..d11bc29b8 100644 --- a/packages/server/modules/core/graph/resolvers/appTokens.ts +++ b/packages/server/modules/core/graph/resolvers/appTokens.ts @@ -21,7 +21,7 @@ const createAppToken = createAppTokenFactory({ storeUserServerAppToken: storeUserServerAppTokenFactory({ db }) }) -export = { +export default { Query: { async authenticatedAsApp(_parent, _args, ctx) { const { appId, token } = ctx diff --git a/packages/server/modules/core/graph/resolvers/base.ts b/packages/server/modules/core/graph/resolvers/base.ts index 8dae2f5c8..645fa7da6 100644 --- a/packages/server/modules/core/graph/resolvers/base.ts +++ b/packages/server/modules/core/graph/resolvers/base.ts @@ -12,7 +12,9 @@ export default { }, Subscription: { ping: { - subscribe: filteredSubscribe(TestSubscriptions.Ping, () => true) + subscribe: filteredSubscribe(TestSubscriptions.Ping, () => { + return true // allow for all subs + }) } } } as Resolvers diff --git a/packages/server/modules/core/graph/resolvers/branches.ts b/packages/server/modules/core/graph/resolvers/branches.ts index 4d914eb9c..2c7b9be2e 100644 --- a/packages/server/modules/core/graph/resolvers/branches.ts +++ b/packages/server/modules/core/graph/resolvers/branches.ts @@ -34,7 +34,7 @@ import { import { throwIfResourceAccessNotAllowed } from '@/modules/core/helpers/token' import { withOperationLogging } from '@/observability/domain/businessLogging' -export = { +export default { Query: {}, Stream: { async branches(parent, args) { diff --git a/packages/server/modules/core/graph/resolvers/commits.ts b/packages/server/modules/core/graph/resolvers/commits.ts index 963383837..91874a778 100644 --- a/packages/server/modules/core/graph/resolvers/commits.ts +++ b/packages/server/modules/core/graph/resolvers/commits.ts @@ -125,7 +125,7 @@ const throwIfRateLimited = throwIfRateLimitedFactory({ rateLimiterEnabled: isRateLimiterEnabled() }) -export = { +export default { Query: {}, Commit: { async stream(parent, _args, ctx) { diff --git a/packages/server/modules/core/graph/resolvers/common.ts b/packages/server/modules/core/graph/resolvers/common.ts index e0ccd799c..0450f1c00 100644 --- a/packages/server/modules/core/graph/resolvers/common.ts +++ b/packages/server/modules/core/graph/resolvers/common.ts @@ -4,9 +4,9 @@ import { Resolvers } from '@/modules/core/graph/generated/graphql' import { getProjectDbClient } from '@/modules/multiregion/utils/dbSelector' import { NotImplementedError } from '@/modules/shared/errors' import { isNonNullable } from '@speckle/shared' -import { keyBy } from 'lodash' +import { keyBy } from 'lodash-es' -export = { +export default { SmartTextEditorValue: { async attachments(parent) { const { blobIds, projectId } = parent diff --git a/packages/server/modules/core/graph/resolvers/models.ts b/packages/server/modules/core/graph/resolvers/models.ts index cac888949..06154f5ee 100644 --- a/packages/server/modules/core/graph/resolvers/models.ts +++ b/packages/server/modules/core/graph/resolvers/models.ts @@ -9,7 +9,7 @@ import { getProjectTopLevelModelsTreeFactory } from '@/modules/core/services/branch/retrieval' import { getServerOrigin } from '@/modules/shared/helpers/envHelper' -import { last } from 'lodash' +import { last } from 'lodash-es' import { getViewerResourceGroupsFactory } from '@/modules/core/services/commit/viewerResources' import { @@ -61,7 +61,7 @@ import { throwIfResourceAccessNotAllowed } from '@/modules/core/helpers/token' import { TokenResourceIdentifierType } from '@/modules/core/domain/tokens/types' import { withOperationLogging } from '@/observability/domain/businessLogging' -export = { +export default { User: { async versions(parent, args, ctx) { const authoredOnly = args.authoredOnly diff --git a/packages/server/modules/core/graph/resolvers/objects.ts b/packages/server/modules/core/graph/resolvers/objects.ts index e2f60754f..d3c69186d 100644 --- a/packages/server/modules/core/graph/resolvers/objects.ts +++ b/packages/server/modules/core/graph/resolvers/objects.ts @@ -25,7 +25,7 @@ const getStreamObject: NonNullable['object'] = ) } -export = { +export default { Stream: { object: getStreamObject }, diff --git a/packages/server/modules/core/graph/resolvers/projects.ts b/packages/server/modules/core/graph/resolvers/projects.ts index fcfe32a90..cccdccb05 100644 --- a/packages/server/modules/core/graph/resolvers/projects.ts +++ b/packages/server/modules/core/graph/resolvers/projects.ts @@ -95,7 +95,7 @@ import { ProjectSubscriptions, UserSubscriptions } from '@/modules/shared/utils/subscriptions' -import { has } from 'lodash' +import { has } from 'lodash-es' import { throwIfAuthNotOk } from '@/modules/shared/helpers/errorHelper' import { withOperationLogging } from '@/observability/domain/businessLogging' import { diff --git a/packages/server/modules/core/graph/resolvers/server.ts b/packages/server/modules/core/graph/resolvers/server.ts index 98e92a3cb..ea223dfc6 100644 --- a/packages/server/modules/core/graph/resolvers/server.ts +++ b/packages/server/modules/core/graph/resolvers/server.ts @@ -20,7 +20,7 @@ const updateServerInfo = updateServerInfoFactory({ db }) const getPublicRoles = getPublicRolesFactory({ db }) const getPublicScopes = getPublicScopesFactory({ db }) -export = { +export default { Query: { async serverInfo() { return await getServerInfo() diff --git a/packages/server/modules/core/graph/resolvers/streams.ts b/packages/server/modules/core/graph/resolvers/streams.ts index ffea6d6d4..f7e2bc41f 100644 --- a/packages/server/modules/core/graph/resolvers/streams.ts +++ b/packages/server/modules/core/graph/resolvers/streams.ts @@ -9,7 +9,7 @@ import { inviteUsersToProjectFactory } from '@/modules/serverinvites/services/projectInviteManagement' import { removePrivateFields } from '@/modules/core/helpers/userHelper' -import { get } from 'lodash' +import { get } from 'lodash-es' import { getStreamFactory, createStreamFactory, @@ -238,7 +238,7 @@ const throwIfRateLimited = throwIfRateLimitedFactory({ /** * @type {import('@/modules/core/graph/generated/graphql').Resolvers} */ -export = { +export default { Query: { async stream(_, args, context) { throwIfResourceAccessNotAllowed({ diff --git a/packages/server/modules/core/graph/resolvers/userEmails.ts b/packages/server/modules/core/graph/resolvers/userEmails.ts index f428d3bf3..62d28cb17 100644 --- a/packages/server/modules/core/graph/resolvers/userEmails.ts +++ b/packages/server/modules/core/graph/resolvers/userEmails.ts @@ -44,7 +44,7 @@ const requestNewEmailVerification = requestNewEmailVerificationFactory({ sendEmail }) -export = { +export default { ActiveUserMutations: { emailMutations: () => ({}) }, diff --git a/packages/server/modules/core/graph/resolvers/users.ts b/packages/server/modules/core/graph/resolvers/users.ts index 3cb62b243..c653549b1 100644 --- a/packages/server/modules/core/graph/resolvers/users.ts +++ b/packages/server/modules/core/graph/resolvers/users.ts @@ -95,7 +95,7 @@ const getAdminUsersListCollection = getAdminUsersListCollectionFactory({ getUsers: legacyGetPaginatedUsersFactory({ db }) }) -export = { +export default { Query: { async activeUser(_parent, _args, context) { const activeUserId = context.userId diff --git a/packages/server/modules/core/graph/resolvers/versions.ts b/packages/server/modules/core/graph/resolvers/versions.ts index 9b1e09117..5bd6388d2 100644 --- a/packages/server/modules/core/graph/resolvers/versions.ts +++ b/packages/server/modules/core/graph/resolvers/versions.ts @@ -57,7 +57,7 @@ const throwIfRateLimited = throwIfRateLimitedFactory({ rateLimiterEnabled: isRateLimiterEnabled() }) -export = { +export default { Project: { async version(parent, args, ctx) { const projectDB = await getProjectDbClient({ projectId: parent.id }) diff --git a/packages/server/modules/core/graph/schema.ts b/packages/server/modules/core/graph/schema.ts new file mode 100644 index 000000000..cff8f196e --- /dev/null +++ b/packages/server/modules/core/graph/schema.ts @@ -0,0 +1,4 @@ +import { graphSchema } from '@/modules' + +const schema = await graphSchema() +export default schema diff --git a/packages/server/modules/core/graph/setup.ts b/packages/server/modules/core/graph/setup.ts index 3c24fad70..389cbc35b 100644 --- a/packages/server/modules/core/graph/setup.ts +++ b/packages/server/modules/core/graph/setup.ts @@ -1,8 +1,9 @@ +import { BadRequestError } from '@/modules/shared/errors' +import { isGraphQLError } from '@/modules/shared/helpers/graphqlHelper' import { ApolloServerOptions, BaseContext } from '@apollo/server' -import { Authz } from '@speckle/shared' -import { GraphQLError } from 'graphql' -import _, { isObjectLike } from 'lodash' -import { VError } from 'verror' +import { ensureError } from '@speckle/shared' +import { omit } from 'lodash-es' +import VError from 'verror' import { ZodError } from 'zod' import { fromZodError } from 'zod-validation-error' @@ -23,53 +24,54 @@ export function buildErrorFormatter(params: { // TODO: Add support for client-aware errors and obfuscate everything else return function (formattedError, error) { let realError = error || formattedError - if (realError instanceof GraphQLError && realError.originalError) { + const writableFormattedError = { ...formattedError } + + if (isGraphQLError(realError) && realError.originalError) { realError = realError.originalError } // If error is a ZodError, convert its message to something more readable if (realError instanceof ZodError) { - return { - ...formattedError, - message: fromZodError(realError).message, - extensions: { ...formattedError.extensions, code: 'BAD_REQUEST' } + writableFormattedError.message = fromZodError(realError).message + writableFormattedError.extensions = { + ...(writableFormattedError.extensions || {}), + code: BadRequestError.code } } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let extensions: { [key: string]: any } = { - ...(formattedError.extensions || {}) - } - + // If VError, handle info & stack trace if (realError instanceof VError) { - extensions = _.omit( - { - ...extensions, - ...(VError.info(realError) || {}), - stacktrace: VError.fullStack(realError) - }, - VERROR_TRASH_PROPS - ) - } else if (Authz.isAuthPolicyError(realError)) { - extensions = { - ...extensions, - code: realError.code, - ...(isObjectLike(realError.payload) - ? realError.payload - : { payload: realError.payload }) + writableFormattedError.extensions = { + ...(writableFormattedError.extensions || {}), + ...(VError.info(realError) || {}) } } + // Clean up extensions + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const extensions = writableFormattedError.extensions || ({} as Record) + // Getting rid of redundant info delete extensions.originalError - if (!includeStacktraceInErrorResponses) { - delete extensions.stacktrace + + // Updating exception metadata in extensions + if (extensions.exception) { + extensions.exception = omit(extensions.exception, VERROR_TRASH_PROPS) + + if (includeStacktraceInErrorResponses) { + extensions.exception.stacktrace = + realError instanceof VError + ? VError.fullStack(realError) + : ensureError(realError).stack + } else { + delete extensions.exception.stacktrace + } } return { - message: formattedError.message, - locations: formattedError.locations, - path: formattedError.path, + message: writableFormattedError.message, + locations: writableFormattedError.locations, + path: writableFormattedError.path, extensions } } diff --git a/packages/server/modules/core/helpers/branch.ts b/packages/server/modules/core/helpers/branch.ts index b07621e43..76dd14e7f 100644 --- a/packages/server/modules/core/helpers/branch.ts +++ b/packages/server/modules/core/helpers/branch.ts @@ -4,7 +4,7 @@ import { DeleteModelInput, UpdateModelInput } from '@/modules/core/graph/generated/graphql' -import { has } from 'lodash' +import { has } from 'lodash-es' export const isBranchUpdateInput = ( i: BranchUpdateInput | UpdateModelInput diff --git a/packages/server/modules/core/helpers/meta.ts b/packages/server/modules/core/helpers/meta.ts index 8509a2296..28c0e4b97 100644 --- a/packages/server/modules/core/helpers/meta.ts +++ b/packages/server/modules/core/helpers/meta.ts @@ -2,7 +2,7 @@ import { Nullable } from '@speckle/shared' import { SchemaConfig, MetaSchemaConfig } from '@/modules/core/dbSchema' -import { camelCase, isString } from 'lodash' +import { camelCase, isString } from 'lodash-es' import { Knex } from 'knex' /** diff --git a/packages/server/modules/core/helpers/project.ts b/packages/server/modules/core/helpers/project.ts index fb2e8a114..dd1d64ad2 100644 --- a/packages/server/modules/core/helpers/project.ts +++ b/packages/server/modules/core/helpers/project.ts @@ -5,11 +5,17 @@ import { } from '@/modules/core/graph/generated/graphql' import { ProjectRecordVisibility } from '@/modules/core/helpers/types' import { throwUncoveredError } from '@speckle/shared' -import { has } from 'lodash' +import { has, get } from 'lodash-es' export const isProjectCreateInput = ( i: StreamCreateInput | ProjectCreateArgs -): i is ProjectCreateArgs => has(i, 'visibility') +): i is ProjectCreateArgs => { + if (!has(i, 'visibility')) return false + + // If its lowercase, its not actually the project create input but the project itself - common mistake in tests + const visibility = get(i, 'visibility') as string + return visibility.toUpperCase() === visibility +} export const mapGqlToDbProjectVisibility = ( visibility: ProjectVisibility diff --git a/packages/server/modules/core/helpers/server.ts b/packages/server/modules/core/helpers/server.ts index 26a6ce228..62974064f 100644 --- a/packages/server/modules/core/helpers/server.ts +++ b/packages/server/modules/core/helpers/server.ts @@ -1,7 +1,7 @@ import { getServerOrigin } from '@/modules/shared/helpers/envHelper' import type { Request } from 'express' import type { IncomingMessage } from 'http' -import { get } from 'lodash' +import { get } from 'lodash-es' import { parse } from 'url' export const getRequestPath = (req: IncomingMessage | Request) => { diff --git a/packages/server/modules/core/helpers/token.ts b/packages/server/modules/core/helpers/token.ts index ee3bb8ba5..eec493b98 100644 --- a/packages/server/modules/core/helpers/token.ts +++ b/packages/server/modules/core/helpers/token.ts @@ -14,7 +14,7 @@ import { Scopes, ServerScope } from '@speckle/shared' -import { differenceBy } from 'lodash' +import { differenceBy } from 'lodash-es' export enum RoleResourceTargets { Streams = 'streams', diff --git a/packages/server/modules/core/helpers/userHelper.ts b/packages/server/modules/core/helpers/userHelper.ts index 887dcb815..911a452dc 100644 --- a/packages/server/modules/core/helpers/userHelper.ts +++ b/packages/server/modules/core/helpers/userHelper.ts @@ -1,5 +1,5 @@ import { LimitedUserRecord, UserRecord } from '@/modules/core/helpers/types' -import { pick } from 'lodash' +import { pick } from 'lodash-es' /** * Fields from the entity that users can see about other users diff --git a/packages/server/modules/core/index.ts b/packages/server/modules/core/index.ts index 22d5299b1..253d2f6bd 100644 --- a/packages/server/modules/core/index.ts +++ b/packages/server/modules/core/index.ts @@ -98,8 +98,7 @@ const coreModule: SpeckleModule<{ // Setup test subs if (isTestEnv()) { - const { startEmittingTestSubs } = - require('@/test/graphqlHelper') as typeof import('@/test/graphqlHelper') + const { startEmittingTestSubs } = await import('@/test/graphqlHelper') stopTestSubs = await startEmittingTestSubs() } @@ -147,4 +146,4 @@ const coreModule: SpeckleModule<{ } } -export = coreModule +export default coreModule diff --git a/packages/server/modules/core/loaders.ts b/packages/server/modules/core/loaders.ts index eb4312a55..592ffe710 100644 --- a/packages/server/modules/core/loaders.ts +++ b/packages/server/modules/core/loaders.ts @@ -4,7 +4,7 @@ import { graphDataloadersBuilders } from '@/modules/index' import { ModularizedDataLoadersConstraint } from '@/modules/shared/helpers/graphqlHelper' import { Knex } from 'knex' import { isNonNullable, Optional } from '@speckle/shared' -import { flatten, noop, isFunction } from 'lodash' +import { flatten, noop, isFunction } from 'lodash-es' import { db } from '@/db/knex' /** @@ -73,7 +73,7 @@ export async function buildRequestLoaders( options?: Partial<{ cleanLoadersEarly: boolean }> ) { const createLoader = buildDataLoaderCreator(options?.cleanLoadersEarly || false) - const modulesLoaders = graphDataloadersBuilders() + const modulesLoaders = await graphDataloadersBuilders() const mainDb = db diff --git a/packages/server/modules/core/migrations/000-core.js b/packages/server/modules/core/migrations/000-core.js index 0da1128aa..cd7ec088f 100644 --- a/packages/server/modules/core/migrations/000-core.js +++ b/packages/server/modules/core/migrations/000-core.js @@ -2,7 +2,7 @@ 'use strict' // Knex table migrations -exports.up = async (knex) => { +const up = async (knex) => { await knex.raw('CREATE EXTENSION IF NOT EXISTS "pgcrypto"') // Base table holding up some configuration variables for the server. Not really used much. @@ -260,7 +260,7 @@ exports.up = async (knex) => { }) } -exports.down = async (knex) => { +const down = async (knex) => { await knex.schema.dropTableIfExists('server_config') await knex.schema.dropTableIfExists('server_acl') @@ -286,3 +286,5 @@ exports.down = async (knex) => { await knex.raw('DROP TYPE IF EXISTS speckle_reference_type ') await knex.raw('DROP TYPE IF EXISTS speckle_acl_role_type ') } + +export { up, down } diff --git a/packages/server/modules/core/migrations/20201222100048_add_sourceapp_to_commits.js b/packages/server/modules/core/migrations/20201222100048_add_sourceapp_to_commits.js index 8d2f7a542..a4861e22d 100644 --- a/packages/server/modules/core/migrations/20201222100048_add_sourceapp_to_commits.js +++ b/packages/server/modules/core/migrations/20201222100048_add_sourceapp_to_commits.js @@ -1,12 +1,14 @@ /* istanbul ignore file */ -exports.up = async (knex) => { +const up = async (knex) => { await knex.schema.alterTable('commits', (table) => { table.string('sourceApplication', 1024) }) } -exports.down = async (knex) => { +const down = async (knex) => { await knex.schema.alterTable('commits', (table) => { table.dropColumn('sourceApplication') }) } + +export { up, down } diff --git a/packages/server/modules/core/migrations/20201222101522_add_totalchildrencount_to_commits.js b/packages/server/modules/core/migrations/20201222101522_add_totalchildrencount_to_commits.js index b6168672b..d9b53b624 100644 --- a/packages/server/modules/core/migrations/20201222101522_add_totalchildrencount_to_commits.js +++ b/packages/server/modules/core/migrations/20201222101522_add_totalchildrencount_to_commits.js @@ -1,12 +1,14 @@ /* istanbul ignore file */ -exports.up = async (knex) => { +const up = async (knex) => { await knex.schema.alterTable('commits', (table) => { table.integer('totalChildrenCount') }) } -exports.down = async (knex) => { +const down = async (knex) => { await knex.schema.alterTable('commits', (table) => { table.dropColumn('totalChildrenCount') }) } + +export { up, down } diff --git a/packages/server/modules/core/migrations/20201223120532_add_commit_parents_simplification.js b/packages/server/modules/core/migrations/20201223120532_add_commit_parents_simplification.js index 20e76ccdb..62568599a 100644 --- a/packages/server/modules/core/migrations/20201223120532_add_commit_parents_simplification.js +++ b/packages/server/modules/core/migrations/20201223120532_add_commit_parents_simplification.js @@ -1,15 +1,17 @@ /* istanbul ignore file */ -exports.up = async (knex) => { +const up = async (knex) => { await knex.schema.dropTableIfExists('parent_commits') await knex.schema.alterTable('commits', (table) => { table.specificType('parents', 'text[]') }) } -exports.down = async (knex) => { +const down = async (knex) => { const hasColumn = await knex.schema.hasColumn('commits', 'parents') if (hasColumn) await knex.schema.alterTable('commits', (table) => { table.dropColumn('parents') }) } + +export { up, down } diff --git a/packages/server/modules/core/migrations/20201230111428_add_scopes_public_field.js b/packages/server/modules/core/migrations/20201230111428_add_scopes_public_field.js index 536a0c8a9..d752caade 100644 --- a/packages/server/modules/core/migrations/20201230111428_add_scopes_public_field.js +++ b/packages/server/modules/core/migrations/20201230111428_add_scopes_public_field.js @@ -1,11 +1,11 @@ /* istanbul ignore file */ -exports.up = async (knex) => { +const up = async (knex) => { await knex.schema.alterTable('scopes', (table) => { table.boolean('public').defaultTo(true) }) } -exports.down = async (knex) => { +const down = async (knex) => { const hasColumn = await knex.schema.hasColumn('scopes', 'public') if (hasColumn) { await knex.schema.alterTable('scopes', (table) => { @@ -13,3 +13,5 @@ exports.down = async (knex) => { }) } } + +export { up, down } diff --git a/packages/server/modules/core/migrations/20210225130308_add_roles_public_field.js b/packages/server/modules/core/migrations/20210225130308_add_roles_public_field.js index 823914069..45e5c5d19 100644 --- a/packages/server/modules/core/migrations/20210225130308_add_roles_public_field.js +++ b/packages/server/modules/core/migrations/20210225130308_add_roles_public_field.js @@ -1,11 +1,11 @@ /* istanbul ignore file */ -exports.up = async (knex) => { +const up = async (knex) => { await knex.schema.alterTable('user_roles', (table) => { table.boolean('public').defaultTo(true) }) } -exports.down = async (knex) => { +const down = async (knex) => { const hasColumn = await knex.schema.hasColumn('user_roles', 'public') if (hasColumn) { await knex.schema.alterTable('user_roles', (table) => { @@ -13,3 +13,5 @@ exports.down = async (knex) => { }) } } + +export { up, down } diff --git a/packages/server/modules/core/migrations/20210314101154_add_invitefield_to_serverinfo.js b/packages/server/modules/core/migrations/20210314101154_add_invitefield_to_serverinfo.js index c42303bd9..302d31151 100644 --- a/packages/server/modules/core/migrations/20210314101154_add_invitefield_to_serverinfo.js +++ b/packages/server/modules/core/migrations/20210314101154_add_invitefield_to_serverinfo.js @@ -1,11 +1,11 @@ // /* istanbul ignore file */ -exports.up = async (knex) => { +const up = async (knex) => { await knex.schema.alterTable('server_config', (table) => { table.boolean('inviteOnly').defaultTo(false) }) } -exports.down = async (knex) => { +const down = async (knex) => { const hasColumn = await knex.schema.hasColumn('server_config', 'inviteOnly') if (hasColumn) { await knex.schema.alterTable('server_config', (table) => { @@ -13,3 +13,5 @@ exports.down = async (knex) => { }) } } + +export { up, down } diff --git a/packages/server/modules/core/migrations/20210322190000_add_streamid_to_objects.js b/packages/server/modules/core/migrations/20210322190000_add_streamid_to_objects.js index 793538711..e20888b9c 100644 --- a/packages/server/modules/core/migrations/20210322190000_add_streamid_to_objects.js +++ b/packages/server/modules/core/migrations/20210322190000_add_streamid_to_objects.js @@ -9,7 +9,7 @@ // Set streamId as notNullable in closures // delete composite index (streamId, id) and Create composite primary key on the same fields (unique index was used as a workaround bc we can't have composite PK with null values) */ -exports.up = async (knex) => { +const up = async (knex) => { await knex.schema.alterTable('commits', (table) => { table.dropForeign('referencedObject') }) @@ -110,7 +110,7 @@ exports.up = async (knex) => { /* Revert data and schema */ -exports.down = async (knex) => { +const down = async (knex) => { const hasColumn = await knex.schema.hasColumn('objects', 'streamId') if (hasColumn) { await knex.schema.alterTable('objects', (table) => { @@ -193,3 +193,5 @@ exports.down = async (knex) => { }) } } + +export { up, down } diff --git a/packages/server/modules/core/migrations/20210603160000_optional_user_references.js b/packages/server/modules/core/migrations/20210603160000_optional_user_references.js index 99017a25d..5e2822d46 100644 --- a/packages/server/modules/core/migrations/20210603160000_optional_user_references.js +++ b/packages/server/modules/core/migrations/20210603160000_optional_user_references.js @@ -1,5 +1,5 @@ // /* istanbul ignore file */ -exports.up = async (knex) => { +const up = async (knex) => { await knex.raw('ALTER TABLE commits ALTER COLUMN "author" DROP NOT NULL;') await knex.raw('ALTER TABLE commits DROP CONSTRAINT commits_author_foreign;') await knex.raw(` @@ -20,7 +20,7 @@ exports.up = async (knex) => { `) } -exports.down = async () => { +const down = async () => { // NOTE: // This migration cannot run backwards: if a user deletes their account, the previous not null // constraint cannot be satisfied. Therefore, there's no going back (and there isn't really a need either). @@ -40,3 +40,5 @@ exports.down = async () => { // ` ) // await knex.raw( 'ALTER TABLE commits ALTER COLUMN "author" SET NOT NULL;' ) } + +export { up, down } diff --git a/packages/server/modules/core/migrations/20211119105730_de_duplicate_users.js b/packages/server/modules/core/migrations/20211119105730_de_duplicate_users.js index 0b4681eec..d6e11afe7 100644 --- a/packages/server/modules/core/migrations/20211119105730_de_duplicate_users.js +++ b/packages/server/modules/core/migrations/20211119105730_de_duplicate_users.js @@ -1,12 +1,12 @@ // /* istanbul ignore file */ +import roles from '@/modules/core/roles' + /* This migration is fixing the duplicate user problem reported https://speckle.community/t/error-in-grasshopper-while-receiving-data-you-dont-have-access-to-stream-xxxxx-on-server-https-speckle-xyz-or-the-stream-does-not-exist/2003 */ -exports.up = async (knex) => { - const roles = require('@/modules/core/roles') - +const up = async (knex) => { const Users = () => knex('users') // tableName, columnName that need migration @@ -120,4 +120,6 @@ exports.up = async (knex) => { await runMigrations() } -exports.down = async () => {} +const down = async () => {} + +export { up, down } diff --git a/packages/server/modules/core/migrations/20220315140000_ratelimit.js b/packages/server/modules/core/migrations/20220315140000_ratelimit.js index 012991415..16165545d 100644 --- a/packages/server/modules/core/migrations/20220315140000_ratelimit.js +++ b/packages/server/modules/core/migrations/20220315140000_ratelimit.js @@ -1,5 +1,5 @@ /* istanbul ignore file */ -exports.up = async (knex) => { +const up = async (knex) => { await knex.schema.createTable('ratelimit_actions', (table) => { table.timestamp('timestamp').defaultTo(knex.fn.now()) table.string('action').notNullable() @@ -12,9 +12,11 @@ exports.up = async (knex) => { }) } -exports.down = async (knex) => { +const down = async (knex) => { await knex.schema.alterTable('users', (table) => { table.dropColumn('ip') }) await knex.schema.dropTableIfExists('ratelimit_actions') } + +export { up, down } diff --git a/packages/server/modules/core/migrations/20220318121405_add_stream_favorites.js b/packages/server/modules/core/migrations/20220318121405_add_stream_favorites.js index 839bdfc9e..ae1c678e0 100644 --- a/packages/server/modules/core/migrations/20220318121405_add_stream_favorites.js +++ b/packages/server/modules/core/migrations/20220318121405_add_stream_favorites.js @@ -6,7 +6,7 @@ const TABLE_NAME = 'stream_favorites' * @param { import("knex").Knex } knex * @returns { Promise } */ -exports.up = async function (knex) { +const up = async function (knex) { await knex.schema.createTable(TABLE_NAME, (table) => { table.string('streamId', 10).references('id').inTable('streams').onDelete('cascade') table.string('userId', 10).references('id').inTable('users').onDelete('cascade') @@ -26,6 +26,8 @@ exports.up = async function (knex) { * @param { import("knex").Knex } knex * @returns { Promise } */ -exports.down = async function (knex) { +const down = async function (knex) { await knex.schema.dropTableIfExists(TABLE_NAME) } + +export { up, down } diff --git a/packages/server/modules/core/migrations/20220412150558_stream-public-comments.js b/packages/server/modules/core/migrations/20220412150558_stream-public-comments.js index f2b79d26b..7e38f0c6a 100644 --- a/packages/server/modules/core/migrations/20220412150558_stream-public-comments.js +++ b/packages/server/modules/core/migrations/20220412150558_stream-public-comments.js @@ -1,12 +1,14 @@ /* istanbul ignore file */ -exports.up = async (knex) => { +const up = async (knex) => { await knex.schema.alterTable('streams', (table) => { table.boolean('allowPublicComments').defaultTo(false) }) } -exports.down = async (knex) => { +const down = async (knex) => { await knex.schema.alterTable('streams', (table) => { table.dropColumn('allowPublicComments') }) } + +export { up, down } diff --git a/packages/server/modules/core/migrations/20220707135553_make_users_email_not_nullable.js b/packages/server/modules/core/migrations/20220707135553_make_users_email_not_nullable.js index f590f77d5..0efac6563 100644 --- a/packages/server/modules/core/migrations/20220707135553_make_users_email_not_nullable.js +++ b/packages/server/modules/core/migrations/20220707135553_make_users_email_not_nullable.js @@ -1,11 +1,11 @@ -const { Users } = require('@/modules/core/dbSchema') +const TABLE_NAME = 'users' /** * @param { import("knex").Knex } knex * @returns { Promise } */ -exports.up = async function (knex) { - await knex.schema.alterTable(Users.name, (table) => { +const up = async function (knex) { + await knex.schema.alterTable(TABLE_NAME, (table) => { table.string('email').notNullable().alter() }) } @@ -14,8 +14,10 @@ exports.up = async function (knex) { * @param { import("knex").Knex } knex * @returns { Promise } */ -exports.down = async function (knex) { - await knex.schema.alterTable(Users.name, (table) => { +const down = async function (knex) { + await knex.schema.alterTable(TABLE_NAME, (table) => { table.string('email').nullable().alter() }) } + +export { up, down } diff --git a/packages/server/modules/core/migrations/readme.md b/packages/server/modules/core/migrations/readme.md index 47a41c550..4b0593e76 100644 --- a/packages/server/modules/core/migrations/readme.md +++ b/packages/server/modules/core/migrations/readme.md @@ -6,13 +6,13 @@ Next, write your migration! Here's an example below that adds a new column to a ```js /* istanbul ignore file */ -exports.up = async (knex) => { +const up = async (knex) => { await knex.schema.alterTable('scopes', (table) => { table.boolean('public').defaultTo(true) }) } -exports.down = async (knex) => { +const down = async (knex) => { let hasColumn = await knex.schema.hasColumn('scopes', 'public') if (hasColumn) { await knex.schema.alterTable('scopes', (table) => { @@ -20,6 +20,8 @@ exports.down = async (knex) => { }) } } + +export { up, down } ``` Notes: diff --git a/packages/server/modules/core/repositories/branches.ts b/packages/server/modules/core/repositories/branches.ts index 0c4ec62c9..565c66b1f 100644 --- a/packages/server/modules/core/repositories/branches.ts +++ b/packages/server/modules/core/repositories/branches.ts @@ -17,7 +17,7 @@ import { } from '@/modules/shared/helpers/dbHelper' import crs from 'crypto-random-string' import { Knex } from 'knex' -import { clamp, isUndefined, last, trim } from 'lodash' +import { clamp, isUndefined, last, trim } from 'lodash-es' import { getMaximumProjectModelsPerPage } from '@/modules/shared/helpers/envHelper' import { DeleteBranchById, diff --git a/packages/server/modules/core/repositories/commits.ts b/packages/server/modules/core/repositories/commits.ts index fcf9ebfea..d09056ad4 100644 --- a/packages/server/modules/core/repositories/commits.ts +++ b/packages/server/modules/core/repositories/commits.ts @@ -15,7 +15,7 @@ import { StreamAclRecord, StreamCommitRecord } from '@/modules/core/helpers/types' -import { clamp, uniq, uniqBy, reduce, keyBy, mapValues } from 'lodash' +import { clamp, uniq, uniqBy, reduce, keyBy, mapValues } from 'lodash-es' import crs from 'crypto-random-string' import { BatchedSelectOptions, diff --git a/packages/server/modules/core/repositories/embedTokens.ts b/packages/server/modules/core/repositories/embedTokens.ts index e8459cca4..1ae6cb50d 100644 --- a/packages/server/modules/core/repositories/embedTokens.ts +++ b/packages/server/modules/core/repositories/embedTokens.ts @@ -10,7 +10,7 @@ import { } from '@/modules/core/domain/tokens/operations' import { UserInputError } from '@/modules/core/errors/userinput' import { Knex } from 'knex' -import { clamp } from 'lodash' +import { clamp } from 'lodash-es' const tables = { apiTokens: (db: Knex) => db(ApiTokens.name), diff --git a/packages/server/modules/core/repositories/objects.ts b/packages/server/modules/core/repositories/objects.ts index 5da1e3be2..468d6fd7f 100644 --- a/packages/server/modules/core/repositories/objects.ts +++ b/packages/server/modules/core/repositories/objects.ts @@ -23,7 +23,7 @@ import { } from '@/modules/core/domain/objects/operations' import { SpeckleObject } from '@/modules/core/domain/objects/types' import { SetOptional } from 'type-fest' -import { get, set, toNumber } from 'lodash' +import { get, set, toNumber } from 'lodash-es' import { UserInputError } from '@/modules/core/errors/userinput' const tables = { diff --git a/packages/server/modules/core/repositories/streams.ts b/packages/server/modules/core/repositories/streams.ts index ec9a12f95..074566de7 100644 --- a/packages/server/modules/core/repositories/streams.ts +++ b/packages/server/modules/core/repositories/streams.ts @@ -1,4 +1,4 @@ -import _, { +import { clamp, groupBy, has, @@ -10,8 +10,9 @@ import _, { omit, omitBy, reduce, - toNumber -} from 'lodash' + toNumber, + keyBy +} from 'lodash-es' import { Streams, StreamAcl, @@ -270,7 +271,7 @@ export const getFavoritedStreamsPageFactory = (deps: { db: Knex }): GetFavoritedStreamsPage => async (params) => { const { userId, cursor, limit, streamIdWhitelist } = params - const finalLimit = _.clamp(limit || 25, 1, 25) + const finalLimit = clamp(limit || 25, 1, 25) const query = getFavoritedStreamsQueryBaseFactory(deps)(userId, streamIdWhitelist) query .select< @@ -366,7 +367,7 @@ export const getBatchUserFavoriteDataFactory = .whereIn(StreamFavorites.col.streamId, streamIds) const rows = await query - return _.keyBy(rows, 'streamId') + return keyBy(rows, 'streamId') } /** @@ -385,7 +386,7 @@ export const getBatchStreamFavoritesCountsFactory = .groupBy(StreamFavorites.col.streamId) const rows = await query - return _.mapValues(_.keyBy(rows, 'streamId'), (r) => parseInt(r?.count || '0')) + return mapValues(keyBy(rows, 'streamId'), (r) => parseInt(r?.count || '0')) } /** @@ -443,7 +444,7 @@ export const getOwnedFavoritesCountByUserIdsFactory = .groupBy(StreamAcl.col.userId) const results = await query - return _.mapValues(_.keyBy(results, 'userId'), (r) => parseInt(r?.count || '0')) + return mapValues(keyBy(results, 'userId'), (r) => parseInt(r?.count || '0')) } /** @@ -466,8 +467,8 @@ export const getStreamRolesFactory = .whereIn(Streams.col.id, streamIds) const results = await q - return _.mapValues( - _.keyBy(results, (r) => r.id), + return mapValues( + keyBy(results, (r) => r.id), (v) => v.role ) } @@ -967,7 +968,7 @@ export const getUserStreamCountsFactory = } const results = await q - return _.mapValues(_.keyBy(results, 'userId'), (r) => parseInt(r.count)) + return mapValues(keyBy(results, 'userId'), (r) => parseInt(r.count)) } export const deleteStreamFactory = diff --git a/packages/server/modules/core/repositories/userEmails.ts b/packages/server/modules/core/repositories/userEmails.ts index 30e5b5c1c..1e3a80b4c 100644 --- a/packages/server/modules/core/repositories/userEmails.ts +++ b/packages/server/modules/core/repositories/userEmails.ts @@ -18,7 +18,7 @@ import { UserEmailPrimaryAlreadyExistsError, UserEmailPrimaryUnverifiedError } from '@/modules/core/errors/userEmails' -import { get, omit } from 'lodash' +import { get, omit } from 'lodash-es' const whereEmailIs = (query: Knex.QueryBuilder, email: string) => { query.whereRaw('lower("email") = lower(?)', [email]) diff --git a/packages/server/modules/core/repositories/users.ts b/packages/server/modules/core/repositories/users.ts index 8a2b1964e..9c963cfc8 100644 --- a/packages/server/modules/core/repositories/users.ts +++ b/packages/server/modules/core/repositories/users.ts @@ -15,7 +15,7 @@ import { UserWithRole } from '@/modules/core/helpers/types' import { Nullable } from '@/modules/shared/helpers/typeHelper' -import { clamp, isArray, omit } from 'lodash' +import { clamp, isArray, omit } from 'lodash-es' import { metaHelpers } from '@/modules/core/helpers/meta' import { UserValidationError } from '@/modules/core/errors/user' import { Knex } from 'knex' diff --git a/packages/server/modules/core/repositories/versions.ts b/packages/server/modules/core/repositories/versions.ts index b7408888a..f0109665a 100644 --- a/packages/server/modules/core/repositories/versions.ts +++ b/packages/server/modules/core/repositories/versions.ts @@ -1,7 +1,7 @@ import { BranchCommits, knex, Branches, Commits } from '@/modules/core/dbSchema' import { Version } from '@/modules/core/domain/commits/types' import { Knex } from 'knex' -import { groupBy } from 'lodash' +import { groupBy } from 'lodash-es' export const getLastVersionsByProjectIdFactory = ({ db }: { db: Knex }) => diff --git a/packages/server/modules/core/rest/defaultErrorHandler.ts b/packages/server/modules/core/rest/defaultErrorHandler.ts index 9f46465b6..a6f9345b4 100644 --- a/packages/server/modules/core/rest/defaultErrorHandler.ts +++ b/packages/server/modules/core/rest/defaultErrorHandler.ts @@ -3,8 +3,8 @@ import { isDevEnv } from '@/modules/shared/helpers/envHelper' import { getCause } from '@/modules/shared/helpers/errorHelper' import { Optional, ensureError } from '@speckle/shared' import { ErrorRequestHandler } from 'express' -import { get, isNumber } from 'lodash' -import { VError } from 'verror' +import { get, isNumber } from 'lodash-es' +import VError from 'verror' import { logger as defaultLogger } from '@/observability/logging' export const resolveStatusCode = (e: Error): number => { diff --git a/packages/server/modules/core/rest/diffUpload.ts b/packages/server/modules/core/rest/diffUpload.ts index e3b738419..0377d19d8 100644 --- a/packages/server/modules/core/rest/diffUpload.ts +++ b/packages/server/modules/core/rest/diffUpload.ts @@ -1,6 +1,6 @@ import zlib from 'zlib' import { corsMiddlewareFactory } from '@/modules/core/configs/cors' -import { chunk } from 'lodash' +import { chunk } from 'lodash-es' import type { Application } from 'express' import { hasObjectsFactory } from '@/modules/core/repositories/objects' import { validatePermissionsWriteStreamFactory } from '@/modules/core/services/streams/auth' diff --git a/packages/server/modules/core/roles.ts b/packages/server/modules/core/roles.ts index 3d8696154..90549d88e 100644 --- a/packages/server/modules/core/roles.ts +++ b/packages/server/modules/core/roles.ts @@ -4,7 +4,7 @@ import { } from '@/modules/shared/domain/rolesAndScopes/types' import { Roles } from '@/modules/core/helpers/mainConstants' import { RoleInfo } from '@speckle/shared' -import { pick } from 'lodash' +import { pick } from 'lodash-es' // Conventions: // "weight: 1000" => resource owner diff --git a/packages/server/modules/core/services/branch/management.ts b/packages/server/modules/core/services/branch/management.ts index 144313d1c..3e6270af7 100644 --- a/packages/server/modules/core/services/branch/management.ts +++ b/packages/server/modules/core/services/branch/management.ts @@ -13,7 +13,7 @@ import { UpdateModelInput } from '@/modules/core/graph/generated/graphql' import { BranchRecord } from '@/modules/core/helpers/types' -import { has } from 'lodash' +import { has } from 'lodash-es' import { isBranchDeleteInput, isBranchUpdateInput } from '@/modules/core/helpers/branch' import { CreateBranchAndNotify, diff --git a/packages/server/modules/core/services/branch/retrieval.ts b/packages/server/modules/core/services/branch/retrieval.ts index a74f7b4af..bdde16174 100644 --- a/packages/server/modules/core/services/branch/retrieval.ts +++ b/packages/server/modules/core/services/branch/retrieval.ts @@ -4,7 +4,7 @@ import { ProjectModelsTreeArgs, StreamBranchesArgs } from '@/modules/core/graph/generated/graphql' -import { last, sum } from 'lodash' +import { last, sum } from 'lodash-es' import { Merge } from 'type-fest' import { ModelsTreeItemGraphQLReturn } from '@/modules/core/helpers/graphTypes' import { getMaximumProjectModelsPerPage } from '@/modules/shared/helpers/envHelper' diff --git a/packages/server/modules/core/services/commit/batchCommitActions.ts b/packages/server/modules/core/services/commit/batchCommitActions.ts index 66f41fa86..f7da39bc2 100644 --- a/packages/server/modules/core/services/commit/batchCommitActions.ts +++ b/packages/server/modules/core/services/commit/batchCommitActions.ts @@ -24,7 +24,7 @@ import { import { Roles } from '@/modules/core/helpers/mainConstants' import { ensureError } from '@/modules/shared/helpers/errorHelper' import { EventBusEmit } from '@/modules/shared/services/eventBus' -import { difference, groupBy, has, keyBy } from 'lodash' +import { difference, groupBy, has, keyBy } from 'lodash-es' type OldBatchInput = CommitsMoveInput | CommitsDeleteInput type CommitBatchInput = OldBatchInput | MoveVersionsInput | DeleteVersionsInput diff --git a/packages/server/modules/core/services/commit/management.ts b/packages/server/modules/core/services/commit/management.ts index 0d0732312..5d75ed6b6 100644 --- a/packages/server/modules/core/services/commit/management.ts +++ b/packages/server/modules/core/services/commit/management.ts @@ -40,7 +40,7 @@ import { import { BranchRecord, CommitRecord } from '@/modules/core/helpers/types' import { EventBusEmit } from '@/modules/shared/services/eventBus' import { ensureError, Roles } from '@speckle/shared' -import { has } from 'lodash' +import { has } from 'lodash-es' import { BranchNotFoundError } from '@/modules/core/errors/branch' export const markCommitReceivedAndNotifyFactory = diff --git a/packages/server/modules/core/services/commit/retrieval.ts b/packages/server/modules/core/services/commit/retrieval.ts index 554674400..8e3e56f09 100644 --- a/packages/server/modules/core/services/commit/retrieval.ts +++ b/packages/server/modules/core/services/commit/retrieval.ts @@ -21,7 +21,7 @@ import { GetStreamBranchByName } from '@/modules/core/domain/branches/operations import { BranchNotFoundError } from '@/modules/core/errors/branch' import { getAllRegisteredDbClients } from '@/modules/multiregion/utils/dbSelector' import { getTotalVersionCountFactory } from '@/modules/core/repositories/commits' -import { sum } from 'lodash' +import { sum } from 'lodash-es' export const legacyGetPaginatedStreamCommitsFactory = (deps: { diff --git a/packages/server/modules/core/services/commit/viewerResources.ts b/packages/server/modules/core/services/commit/viewerResources.ts index 787ecf3d7..d32ab679c 100644 --- a/packages/server/modules/core/services/commit/viewerResources.ts +++ b/packages/server/modules/core/services/commit/viewerResources.ts @@ -26,7 +26,7 @@ import { } from '@/modules/core/graph/generated/graphql' import { CommitRecord } from '@/modules/core/helpers/types' import { Optional, SpeckleViewer } from '@speckle/shared' -import { flatten, keyBy, reduce, uniq, uniqWith } from 'lodash' +import { flatten, keyBy, reduce, uniq, uniqWith } from 'lodash-es' function isResourceItemEqual(a: ViewerResourceItem, b: ViewerResourceItem) { if (a.modelId !== b.modelId) return false diff --git a/packages/server/modules/core/services/richTextEditorService.ts b/packages/server/modules/core/services/richTextEditorService.ts index e76efa23b..6f47d0eaa 100644 --- a/packages/server/modules/core/services/richTextEditorService.ts +++ b/packages/server/modules/core/services/richTextEditorService.ts @@ -1,5 +1,5 @@ import { JSONContent } from '@tiptap/core' -import { isString, isObjectLike, get, has } from 'lodash' +import { isString, isObjectLike, get, has } from 'lodash-es' import { MaybeNullOrUndefined, RichTextEditor } from '@speckle/shared' const { isDocEmpty, documentToBasicString, convertBasicStringToDocument } = diff --git a/packages/server/modules/core/services/streams/clone.ts b/packages/server/modules/core/services/streams/clone.ts index 56330a697..2c2c37185 100644 --- a/packages/server/modules/core/services/streams/clone.ts +++ b/packages/server/modules/core/services/streams/clone.ts @@ -7,7 +7,7 @@ import { import { StreamWithOptionalRole } from '@/modules/core/repositories/streams' import { UserWithOptionalRole } from '@/modules/core/repositories/users' import { generateCommitId } from '@/modules/core/repositories/commits' -import { chunk } from 'lodash' +import { chunk } from 'lodash-es' import { generateBranchId } from '@/modules/core/repositories/branches' import { generateCommentId } from '@/modules/comments/repositories/comments' import dayjs from 'dayjs' diff --git a/packages/server/modules/core/services/streams/discoverableStreams.ts b/packages/server/modules/core/services/streams/discoverableStreams.ts index 02fd06447..d7c37d191 100644 --- a/packages/server/modules/core/services/streams/discoverableStreams.ts +++ b/packages/server/modules/core/services/streams/discoverableStreams.ts @@ -13,7 +13,7 @@ import { import { StreamRecord } from '@/modules/core/helpers/types' import { encodeDiscoverableStreamsCursor } from '@/modules/core/repositories/streams' import { Nullable, Optional } from '@/modules/shared/helpers/typeHelper' -import { clamp } from 'lodash' +import { clamp } from 'lodash-es' type StreamCollection = { cursor: Nullable diff --git a/packages/server/modules/core/services/streams/favorite.ts b/packages/server/modules/core/services/streams/favorite.ts index 07c37ba77..c271da191 100644 --- a/packages/server/modules/core/services/streams/favorite.ts +++ b/packages/server/modules/core/services/streams/favorite.ts @@ -10,7 +10,7 @@ import { import { TokenResourceIdentifierType } from '@/modules/core/domain/tokens/types' import { isResourceAllowed } from '@/modules/core/helpers/token' import { UnauthorizedError } from '@/modules/shared/errors' -import { clamp } from 'lodash' +import { clamp } from 'lodash-es' /** * Get user favorited streams & metadata diff --git a/packages/server/modules/core/services/streams/management.ts b/packages/server/modules/core/services/streams/management.ts index 934cf1ea5..3bcb2fd2b 100644 --- a/packages/server/modules/core/services/streams/management.ts +++ b/packages/server/modules/core/services/streams/management.ts @@ -12,7 +12,7 @@ import { StreamUpdateError } from '@/modules/core/errors/stream' import { isProjectCreateInput } from '@/modules/core/helpers/project' -import { has } from 'lodash' +import { has } from 'lodash-es' import { isNewResourceAllowed } from '@/modules/core/helpers/token' import { TokenResourceIdentifier, diff --git a/packages/server/modules/core/services/tokens.ts b/packages/server/modules/core/services/tokens.ts index 7c484befd..a43dec0ed 100644 --- a/packages/server/modules/core/services/tokens.ts +++ b/packages/server/modules/core/services/tokens.ts @@ -42,7 +42,7 @@ import { decodeIsoDateCursor, encodeIsoDateCursor } from '@/modules/shared/helpers/dbHelper' -import { pick } from 'lodash' +import { pick } from 'lodash-es' import { LogicError } from '@/modules/shared/errors' /* diff --git a/packages/server/modules/core/services/users/legacyAdminUsersList.ts b/packages/server/modules/core/services/users/legacyAdminUsersList.ts index 1defc669a..f46cfa0c9 100644 --- a/packages/server/modules/core/services/users/legacyAdminUsersList.ts +++ b/packages/server/modules/core/services/users/legacyAdminUsersList.ts @@ -12,7 +12,7 @@ import { } from '@/modules/serverinvites/domain/operations' import { ServerInviteRecord } from '@/modules/serverinvites/domain/types' import { resolveTarget } from '@/modules/serverinvites/helpers/core' -import { clamp } from 'lodash' +import { clamp } from 'lodash-es' type LegacyGetUsersInvitesTotalCounts = { userCount: number diff --git a/packages/server/modules/core/services/users/management.ts b/packages/server/modules/core/services/users/management.ts index e086a0805..46404ea22 100644 --- a/packages/server/modules/core/services/users/management.ts +++ b/packages/server/modules/core/services/users/management.ts @@ -36,7 +36,7 @@ import { Roles, ServerRoles } from '@speckle/shared' -import { pick } from 'lodash' +import { pick } from 'lodash-es' import bcrypt from 'bcrypt' import crs from 'crypto-random-string' import { diff --git a/packages/server/modules/core/tests/apitokens.spec.ts b/packages/server/modules/core/tests/apitokens.spec.ts index 3e2a0265d..70b5f2abc 100644 --- a/packages/server/modules/core/tests/apitokens.spec.ts +++ b/packages/server/modules/core/tests/apitokens.spec.ts @@ -20,7 +20,7 @@ import { BasicTestStream, createTestStreams } from '@/test/speckle-helpers/strea import { AllScopes, Roles, Scopes } from '@/modules/core/helpers/mainConstants' import { expect } from 'chai' import cryptoRandomString from 'crypto-random-string' -import { difference } from 'lodash' +import { difference } from 'lodash-es' import type { Express } from 'express' import request from 'supertest' import { createAppFactory } from '@/modules/auth/repositories/apps' diff --git a/packages/server/modules/core/tests/batchCommits.spec.ts b/packages/server/modules/core/tests/batchCommits.spec.ts index 1f70735b0..90de41156 100644 --- a/packages/server/modules/core/tests/batchCommits.spec.ts +++ b/packages/server/modules/core/tests/batchCommits.spec.ts @@ -26,8 +26,7 @@ import { truncateTables } from '@/test/hooks' import { BasicTestCommit, createTestCommits } from '@/test/speckle-helpers/commitHelper' import { BasicTestStream, createTestStreams } from '@/test/speckle-helpers/streamHelper' import { expect } from 'chai' -import { times } from 'lodash' -import { describe } from 'mocha' +import { times } from 'lodash-es' enum BatchActionType { Move, diff --git a/packages/server/modules/core/tests/graph.spec.ts b/packages/server/modules/core/tests/graph.spec.ts index 7481e8b5c..4a2f17d65 100644 --- a/packages/server/modules/core/tests/graph.spec.ts +++ b/packages/server/modules/core/tests/graph.spec.ts @@ -60,7 +60,7 @@ import { getServerInfoFactory } from '@/modules/core/repositories/server' import { getEventBus } from '@/modules/shared/services/eventBus' import { Express } from 'express' import { Server } from 'http' -import { omit } from 'lodash' +import { omit } from 'lodash-es' import { getFeatureFlags } from '@/modules/shared/helpers/envHelper' import { ProjectRecordVisibility } from '@/modules/core/helpers/types' import { BasicTestStream, createTestStream } from '@/test/speckle-helpers/streamHelper' diff --git a/packages/server/modules/core/tests/helpers/creation.ts b/packages/server/modules/core/tests/helpers/creation.ts index a594f0c35..0b38433ce 100644 --- a/packages/server/modules/core/tests/helpers/creation.ts +++ b/packages/server/modules/core/tests/helpers/creation.ts @@ -1,7 +1,7 @@ import cryptoRandomString from 'crypto-random-string' import { Project } from '@/modules/core/domain/streams/types' import { ProjectRecordVisibility } from '@/modules/core/helpers/types' -import { assign } from 'lodash' +import { assign } from 'lodash-es' import { BasicTestCommit } from '@/test/speckle-helpers/commitHelper' import { BasicTestBranch } from '@/test/speckle-helpers/branchHelper' import { BasicTestStream } from '@/test/speckle-helpers/streamHelper' diff --git a/packages/server/modules/core/tests/integration/commits.graph.spec.ts b/packages/server/modules/core/tests/integration/commits.graph.spec.ts index 92c43edf9..144a94033 100644 --- a/packages/server/modules/core/tests/integration/commits.graph.spec.ts +++ b/packages/server/modules/core/tests/integration/commits.graph.spec.ts @@ -1,6 +1,5 @@ import { beforeEachContext } from '@/test/hooks' import { expect } from 'chai' -import { describe, it } from 'mocha' import { createRandomEmail, createRandomPassword diff --git a/packages/server/modules/core/tests/integration/createUser.spec.ts b/packages/server/modules/core/tests/integration/createUser.spec.ts index 7b2d7a374..4a8436830 100644 --- a/packages/server/modules/core/tests/integration/createUser.spec.ts +++ b/packages/server/modules/core/tests/integration/createUser.spec.ts @@ -1,5 +1,4 @@ import { expect } from 'chai' -import { beforeEach, describe, it } from 'mocha' import { beforeEachContext } from '@/test/hooks' import { db } from '@/db/knex' import { diff --git a/packages/server/modules/core/tests/integration/emailVerification.spec.ts b/packages/server/modules/core/tests/integration/emailVerification.spec.ts index 108ed2185..959995288 100644 --- a/packages/server/modules/core/tests/integration/emailVerification.spec.ts +++ b/packages/server/modules/core/tests/integration/emailVerification.spec.ts @@ -1,4 +1,3 @@ -import { describe } from 'mocha' import { createUserEmailFactory, ensureNoPrimaryEmailForUserFactory, diff --git a/packages/server/modules/core/tests/integration/findUsers.spec.ts b/packages/server/modules/core/tests/integration/findUsers.spec.ts index 541aa04eb..32c7900f8 100644 --- a/packages/server/modules/core/tests/integration/findUsers.spec.ts +++ b/packages/server/modules/core/tests/integration/findUsers.spec.ts @@ -1,4 +1,3 @@ -import { describe } from 'mocha' import { createRandomEmail, createRandomPassword diff --git a/packages/server/modules/core/tests/integration/limits.graph.spec.ts b/packages/server/modules/core/tests/integration/limits.graph.spec.ts index 6b77aec54..47e547099 100644 --- a/packages/server/modules/core/tests/integration/limits.graph.spec.ts +++ b/packages/server/modules/core/tests/integration/limits.graph.spec.ts @@ -23,7 +23,7 @@ import { BasicTestCommit, createTestCommits } from '@/test/speckle-helpers/commi import { BasicTestStream, createTestStreams } from '@/test/speckle-helpers/streamHelper' import { expect } from 'chai' import dayjs from 'dayjs' -import { flatten } from 'lodash' +import { flatten } from 'lodash-es' const { FF_PERSONAL_PROJECTS_LIMITS_ENABLED } = getFeatureFlags() diff --git a/packages/server/modules/core/tests/integration/objects.graph.spec.ts b/packages/server/modules/core/tests/integration/objects.graph.spec.ts index b41aa5de4..d1c9303f3 100644 --- a/packages/server/modules/core/tests/integration/objects.graph.spec.ts +++ b/packages/server/modules/core/tests/integration/objects.graph.spec.ts @@ -1,6 +1,5 @@ import { beforeEachContext } from '@/test/hooks' import { expect } from 'chai' -import { describe, it } from 'mocha' import { createRandomEmail, createRandomPassword diff --git a/packages/server/modules/core/tests/integration/projectRepositories.spec.ts b/packages/server/modules/core/tests/integration/projectRepositories.spec.ts index 6eb541f22..f6fc5d542 100644 --- a/packages/server/modules/core/tests/integration/projectRepositories.spec.ts +++ b/packages/server/modules/core/tests/integration/projectRepositories.spec.ts @@ -13,7 +13,7 @@ import { createTestUser } from '@/test/authHelper' import { Roles } from '@speckle/shared' import { expect } from 'chai' import cryptoRandomString from 'crypto-random-string' -import { assign } from 'lodash' +import { assign } from 'lodash-es' const createTestProject = (overrides?: Partial): Project => { const defaults: Project = { diff --git a/packages/server/modules/core/tests/integration/scanTable.spec.ts b/packages/server/modules/core/tests/integration/scanTable.spec.ts index 8781da90c..203f718c0 100644 --- a/packages/server/modules/core/tests/integration/scanTable.spec.ts +++ b/packages/server/modules/core/tests/integration/scanTable.spec.ts @@ -1,4 +1,3 @@ -import { describe } from 'mocha' import { scanTableFactory } from '@/modules/core/helpers/scanTable' import { db } from '@/db/knex' import { UserRecord } from '@/modules/core/helpers/types' diff --git a/packages/server/modules/core/tests/integration/updateUser.spec.ts b/packages/server/modules/core/tests/integration/updateUser.spec.ts index a2e336f5d..1654bae30 100644 --- a/packages/server/modules/core/tests/integration/updateUser.spec.ts +++ b/packages/server/modules/core/tests/integration/updateUser.spec.ts @@ -1,5 +1,4 @@ import { expect } from 'chai' -import { beforeEach, describe, it } from 'mocha' import { beforeEachContext } from '@/test/hooks' import { db } from '@/db/knex' import { diff --git a/packages/server/modules/core/tests/integration/userEmails.graph.spec.ts b/packages/server/modules/core/tests/integration/userEmails.graph.spec.ts index 110f2c375..992ca34ca 100644 --- a/packages/server/modules/core/tests/integration/userEmails.graph.spec.ts +++ b/packages/server/modules/core/tests/integration/userEmails.graph.spec.ts @@ -1,6 +1,5 @@ import { beforeEachContext, truncateTables } from '@/test/hooks' import { expect } from 'chai' -import { describe, it } from 'mocha' import { createRandomEmail, createRandomPassword diff --git a/packages/server/modules/core/tests/integration/userEmails.spec.ts b/packages/server/modules/core/tests/integration/userEmails.spec.ts index c076258d2..88ec95775 100644 --- a/packages/server/modules/core/tests/integration/userEmails.spec.ts +++ b/packages/server/modules/core/tests/integration/userEmails.spec.ts @@ -1,4 +1,3 @@ -import { before } from 'mocha' import { beforeEachContext } from '@/test/hooks' import { expect } from 'chai' import { diff --git a/packages/server/modules/core/tests/integration/versions.graph.spec.ts b/packages/server/modules/core/tests/integration/versions.graph.spec.ts index 4cd234967..2c7b457d7 100644 --- a/packages/server/modules/core/tests/integration/versions.graph.spec.ts +++ b/packages/server/modules/core/tests/integration/versions.graph.spec.ts @@ -1,6 +1,5 @@ import { beforeEachContext } from '@/test/hooks' import { expect } from 'chai' -import { describe, it } from 'mocha' import { createRandomEmail, createRandomPassword, diff --git a/packages/server/modules/core/tests/models.spec.ts b/packages/server/modules/core/tests/models.spec.ts index ade5146f9..0c6749cbf 100644 --- a/packages/server/modules/core/tests/models.spec.ts +++ b/packages/server/modules/core/tests/models.spec.ts @@ -12,8 +12,7 @@ import { testApolloServer, TestApolloServer } from '@/test/graphqlHelper' import { beforeEachContext } from '@/test/hooks' import { BasicTestStream, createTestStreams } from '@/test/speckle-helpers/streamHelper' import { expect } from 'chai' -import { omit } from 'lodash' -import { before, describe } from 'mocha' +import { omit } from 'lodash-es' describe('Models', () => { const me: BasicTestUser = { diff --git a/packages/server/modules/core/tests/objects.spec.ts b/packages/server/modules/core/tests/objects.spec.ts index 49381b823..e94d8b5c6 100644 --- a/packages/server/modules/core/tests/objects.spec.ts +++ b/packages/server/modules/core/tests/objects.spec.ts @@ -2,7 +2,7 @@ /* eslint-disable camelcase */ import { expect } from 'chai' import assert from 'assert' -import { cloneDeep, times, random, padStart } from 'lodash' +import { cloneDeep, times, random, padStart } from 'lodash-es' import { beforeEachContext } from '@/test/hooks' import { getAnIdForThisOnePlease } from '@/test/helpers' @@ -404,7 +404,7 @@ describe('Objects @core-objects', () => { expect(objects.length).to.equal(100) parentObjectId = ids[0] - }).timeout(30000) + }).timeout(3000) it('should query object children, ascending order', async () => { // we're assuming the prev test objects exist @@ -727,22 +727,23 @@ describe('Objects @core-objects', () => { expect(commitChildren.objects.length).to.equal(2) }) - it('should stream objects back', (done) => { + it('should stream objects back', async () => { let tcount = 0 - // eslint-disable-next-line @typescript-eslint/no-floating-promises - getObjectChildrenStream({ streamId: stream.id, objectId: commitId }).then( - (stream) => { - stream.on('data', () => tcount++) - stream.on('end', () => { - expect(tcount).to.equal(3333) - done() - }) - } - ) + + const childrenStream = await getObjectChildrenStream({ + streamId: stream.id, + objectId: commitId + }) + await new Promise((resolve) => { + childrenStream.on('data', () => tcount++) + childrenStream.on('end', () => { + expect(tcount).to.equal(3333) + resolve() + }) + }) }) it('should not deadlock when batch inserting in random order', async function () { - this.timeout(5000) const objs = createManyObjects(5000, 'perlin merlin magic') function shuffleArray(array: Array) { @@ -773,7 +774,7 @@ describe('Objects @core-objects', () => { await promisses[i] } }) -}) +}).timeout(5000) function createManyObjects(num: number, noise: string | number) { num = num || 10000 diff --git a/packages/server/modules/core/tests/projects.spec.ts b/packages/server/modules/core/tests/projects.spec.ts index 0db7f995e..93eff0e72 100644 --- a/packages/server/modules/core/tests/projects.spec.ts +++ b/packages/server/modules/core/tests/projects.spec.ts @@ -1,4 +1,3 @@ -import { before, describe } from 'mocha' import { expect } from 'chai' import { BasicTestUser, createTestUsers } from '@/test/authHelper' import { BasicTestStream, createTestStreams } from '@/test/speckle-helpers/streamHelper' @@ -12,7 +11,7 @@ import { ProjectVisibility } from '@/test/graphql/generated/graphql' import { createTestObject } from '@/test/speckle-helpers/commitHelper' -import { times } from 'lodash' +import { times } from 'lodash-es' import { Roles } from '@speckle/shared' import { getFeatureFlags } from '@/modules/shared/helpers/envHelper' diff --git a/packages/server/modules/core/tests/rest.spec.ts b/packages/server/modules/core/tests/rest.spec.ts index 1669d4094..3ae0bcc43 100644 --- a/packages/server/modules/core/tests/rest.spec.ts +++ b/packages/server/modules/core/tests/rest.spec.ts @@ -8,7 +8,7 @@ import crypto from 'crypto' import { beforeEachContext } from '@/test/hooks' import { createManyObjects } from '@/test/helpers' -import { Scopes } from '@speckle/shared' +import { Scopes, wait } from '@speckle/shared' import { getStreamFactory, createStreamFactory, @@ -478,104 +478,112 @@ describe('Upload/Download Routes @api-rest', () => { expect(res).to.have.status(201) }) - it('Should properly download an object, with all its children, into a application/json response', (done) => { - // eslint-disable-next-line @typescript-eslint/no-floating-promises - new Promise((resolve) => setTimeout(resolve, 1500)) // avoids race condition - .then(() => { - // eslint-disable-next-line @typescript-eslint/no-floating-promises - request(app) - .get(`/objects/${testStream.id}/${parentId}`) - .set('Authorization', userA.token) - .buffer() - .parse((res, cb) => { - const resTyped = res as typeof res & { data: string } - resTyped.data = '' - resTyped.on('data', (chunk) => { - resTyped.data += chunk.toString() - }) - resTyped.on('end', () => { - cb(null, resTyped.data) - }) + it('Should properly download an object, with all its children, into a application/json response', async () => { + await wait(1500) // avoids race condition + + await new Promise((resolve, reject) => { + void request(app) + .get(`/objects/${testStream.id}/${parentId}`) + .set('Authorization', userA.token) + .buffer() + .parse((res, cb) => { + const resTyped = res as typeof res & { data: string } + resTyped.data = '' + resTyped.on('data', (chunk) => { + resTyped.data += chunk.toString() }) - .end((err, res) => { - if (err) done(err) - try { - const o = JSON.parse(res.body) - expect(o.length).to.equal(numObjs + 1) - expect(res).to.be.json - done() - } catch (err) { - done(err) - } + resTyped.on('end', () => { + cb(null, resTyped.data) }) - }) + }) + .end((err, res) => { + // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors + if (err) return reject(err) + try { + const o = JSON.parse(res.body) + expect(o.length).to.equal(numObjs + 1) + expect(res).to.be.json + return resolve() + } catch (err) { + // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors + reject(err) + } + }) + }) }) - it('Should properly download an object, with all its children, into a text/plain response', (done) => { - // eslint-disable-next-line @typescript-eslint/no-floating-promises - request(app) - .get(`/objects/${testStream.id}/${parentId}`) - .set('Authorization', userA.token) - .set('Accept', 'text/plain') - .buffer() - .parse((res, cb) => { - const resTyped = res as typeof res & { data: string } + it('Should properly download an object, with all its children, into a text/plain response', async () => { + await new Promise((resolve, reject) => { + void request(app) + .get(`/objects/${testStream.id}/${parentId}`) + .set('Authorization', userA.token) + .set('Accept', 'text/plain') + .buffer() + .parse((res, cb) => { + const resTyped = res as typeof res & { data: string } - resTyped.data = '' - resTyped.on('data', (chunk) => { - resTyped.data += chunk.toString() + resTyped.data = '' + resTyped.on('data', (chunk) => { + resTyped.data += chunk.toString() + }) + resTyped.on('end', () => { + cb(null, resTyped.data) + }) }) - resTyped.on('end', () => { - cb(null, resTyped.data) + .end((err, res) => { + // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors + if (err) return reject(err) + try { + const o = res.body.split('\n').filter((l: string) => l !== '') + expect(o.length).to.equal(numObjs + 1) + expect(res).to.be.text + return resolve() + } catch (err) { + // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors + reject(err) + } }) - }) - .end((err, res) => { - if (err) done(err) - try { - const o = res.body.split('\n').filter((l: string) => l !== '') - expect(o.length).to.equal(numObjs + 1) - expect(res).to.be.text - done() - } catch (err) { - done(err) - } - }) + }) }) - it('Should properly download a list of objects', (done) => { - const objectIds = [] + it('Should properly download a list of objects', async () => { + const objectIds: string[] = [] for (let i = 0; i < objBatches[0].length; i++) { objectIds.push(objBatches[0][i].id) } - // eslint-disable-next-line @typescript-eslint/no-floating-promises - request(app) - .post(`/api/getobjects/${testStream.id}`) - .set('Authorization', userA.token) - .set('Accept', 'text/plain') - .send({ objects: JSON.stringify(objectIds) }) - .buffer() - .parse((res, cb) => { - const resTyped = res as typeof res & { data: string } - resTyped.data = '' - resTyped.on('data', (chunk) => { - resTyped.data += chunk.toString() + await new Promise((resolve, reject) => { + void request(app) + .post(`/api/getobjects/${testStream.id}`) + .set('Authorization', userA.token) + .set('Accept', 'text/plain') + .send({ objects: JSON.stringify(objectIds) }) + .buffer() + .parse((res, cb) => { + const resTyped = res as typeof res & { data: string } + + resTyped.data = '' + resTyped.on('data', (chunk) => { + resTyped.data += chunk.toString() + }) + resTyped.on('end', () => { + cb(null, resTyped.data) + }) }) - resTyped.on('end', () => { - cb(null, resTyped.data) + .end((err, res) => { + // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors + if (err) return reject(err) + try { + const o = res.body.split('\n').filter((l: string) => l !== '') + expect(o.length).to.equal(objectIds.length) + expect(res).to.be.text + return resolve() + } catch (err) { + // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors + reject(err) + } }) - }) - .end((err, res) => { - if (err) done(err) - try { - const o = res.body.split('\n').filter((l: string) => l !== '') - expect(o.length).to.equal(objectIds.length) - expect(res).to.be.text - done() - } catch (err) { - done(err) - } - }) + }) }) it('Should return nothing if the object is not found', async () => { @@ -601,8 +609,8 @@ describe('Upload/Download Routes @api-rest', () => { expect(response).to.have.status(400) }) - it('Should properly check if the server has a list of objects', (done) => { - const objectIds = [] + it('Should properly check if the server has a list of objects', async () => { + const objectIds: string[] = [] for (let i = 0; i < objBatches[0].length; i++) { objectIds.push(objBatches[0][i].id) } @@ -616,46 +624,49 @@ describe('Upload/Download Routes @api-rest', () => { objectIds.push(fakeId) } - // eslint-disable-next-line @typescript-eslint/no-floating-promises - request(app) - .post(`/api/diff/${testStream.id}`) - .set('Authorization', userA.token) - .send({ objects: JSON.stringify(objectIds) }) - .buffer() - .parse((res, cb) => { - const resTyped = res as typeof res & { data: string } + await new Promise((resolve, reject) => { + void request(app) + .post(`/api/diff/${testStream.id}`) + .set('Authorization', userA.token) + .send({ objects: JSON.stringify(objectIds) }) + .buffer() + .parse((res, cb) => { + const resTyped = res as typeof res & { data: string } - resTyped.data = '' - resTyped.on('data', (chunk) => { - resTyped.data += chunk.toString() + resTyped.data = '' + resTyped.on('data', (chunk) => { + resTyped.data += chunk.toString() + }) + resTyped.on('end', () => { + cb(null, resTyped.data) + }) }) - resTyped.on('end', () => { - cb(null, resTyped.data) + .end((err, res) => { + // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors + if (err) return reject(err) + try { + const o = JSON.parse(res.body) + expect(Object.keys(o).length).to.equal(objectIds.length) + // console.log(JSON.stringify(Object.keys(o), undefined, 4)) + for (let i = 0; i < objBatches[0].length; i++) { + assert( + o[objBatches[0][i].id] === true, + `Server is missing an object: ${objBatches[0][i].id}` + ) + } + for (let i = 0; i < fakeIds.length; i++) { + assert( + o[fakeIds[i]] === false, + 'Server wrongly reports it has an extra object' + ) + } + return resolve() + } catch (err) { + // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors + reject(err) + } }) - }) - .end((err, res) => { - if (err) done(err) - try { - const o = JSON.parse(res.body) - expect(Object.keys(o).length).to.equal(objectIds.length) - // console.log(JSON.stringify(Object.keys(o), undefined, 4)) - for (let i = 0; i < objBatches[0].length; i++) { - assert( - o[objBatches[0][i].id] === true, - `Server is missing an object: ${objBatches[0][i].id}` - ) - } - for (let i = 0; i < fakeIds.length; i++) { - assert( - o[fakeIds[i]] === false, - 'Server wrongly reports it has an extra object' - ) - } - done() - } catch (err) { - done(err) - } - }) + }) }) it('Should return status code 400 if the list of objects is not parseable', async () => { diff --git a/packages/server/modules/core/tests/streams.spec.ts b/packages/server/modules/core/tests/streams.spec.ts index 3de29147d..e00ebd53d 100644 --- a/packages/server/modules/core/tests/streams.spec.ts +++ b/packages/server/modules/core/tests/streams.spec.ts @@ -26,7 +26,7 @@ import { revokeStreamPermissionsFactory, updateStreamFactory } from '@/modules/core/repositories/streams' -import { has, times } from 'lodash' +import { has, times } from 'lodash-es' import { Streams } from '@/modules/core/dbSchema' import { Nullable } from '@/modules/shared/helpers/typeHelper' import { sleep } from '@/test/helpers' diff --git a/packages/server/modules/core/tests/unit/scheduledTasks.spec.ts b/packages/server/modules/core/tests/unit/scheduledTasks.spec.ts index b337667d5..9631f44ae 100644 --- a/packages/server/modules/core/tests/unit/scheduledTasks.spec.ts +++ b/packages/server/modules/core/tests/unit/scheduledTasks.spec.ts @@ -1,4 +1,3 @@ -import { describe } from 'mocha' import { ensureError } from '@/modules/shared/helpers/errorHelper' import { scheduledCallbackWrapper, diff --git a/packages/server/modules/core/tests/usersAdminList.spec.ts b/packages/server/modules/core/tests/usersAdminList.spec.ts index df292882c..68fac78f0 100644 --- a/packages/server/modules/core/tests/usersAdminList.spec.ts +++ b/packages/server/modules/core/tests/usersAdminList.spec.ts @@ -1,6 +1,6 @@ import { ServerInvites, Streams, Users } from '@/modules/core/dbSchema' import { truncateTables } from '@/test/hooks' -import { times, clamp } from 'lodash' +import { times, clamp } from 'lodash-es' import { createStreamInviteDirectly } from '@/test/speckle-helpers/inviteHelper' import { getAdminUsersList } from '@/test/graphql/users' import { buildApolloServer } from '@/app' diff --git a/packages/server/modules/core/tests/usersGraphql.spec.ts b/packages/server/modules/core/tests/usersGraphql.spec.ts index 4d10b4f0b..c3eecff65 100644 --- a/packages/server/modules/core/tests/usersGraphql.spec.ts +++ b/packages/server/modules/core/tests/usersGraphql.spec.ts @@ -13,7 +13,6 @@ import { findEmailFactory } from '@/modules/core/repositories/userEmails' import { db } from '@/db/knex' -import { before } from 'mocha' import { createAuthedTestContext, createTestContext, diff --git a/packages/server/modules/core/tests/versions.spec.ts b/packages/server/modules/core/tests/versions.spec.ts index e0b249c31..dc91ff46b 100644 --- a/packages/server/modules/core/tests/versions.spec.ts +++ b/packages/server/modules/core/tests/versions.spec.ts @@ -16,8 +16,7 @@ import { import { createTestObject } from '@/test/speckle-helpers/commitHelper' import { BasicTestStream, createTestStreams } from '@/test/speckle-helpers/streamHelper' import { expect } from 'chai' -import { omit } from 'lodash' -import { before, describe } from 'mocha' +import { omit } from 'lodash-es' describe('Versions', () => { const me: BasicTestUser = { diff --git a/packages/server/modules/core/utils/dbNotificationListener.ts b/packages/server/modules/core/utils/dbNotificationListener.ts index 2986dcd13..57669de31 100644 --- a/packages/server/modules/core/utils/dbNotificationListener.ts +++ b/packages/server/modules/core/utils/dbNotificationListener.ts @@ -1,6 +1,6 @@ import { MaybeAsync, Optional, md5, wait } from '@speckle/shared' import { dbNotificationLogger } from '@/observability/logging' -import { Client, Notification } from 'pg' +import pg from 'pg' import { createRedisClient } from '@/modules/shared/redis/redis' import { getRedisUrl, @@ -11,16 +11,15 @@ import Redis from 'ioredis' import { LogicError, MisconfiguredEnvironmentError } from '@/modules/shared/errors' import { mainDb } from '@/db/knex' import { PartialDeep } from 'type-fest' -import { merge } from 'lodash' +import { merge } from 'lodash-es' import { getConnectionSettings, obfuscateConnectionString } from '@speckle/shared/environment/db' import { getMainRegionConfig } from '@/modules/multiregion/regionConfig' -import type pg from 'pg' import { isMultiRegionEnabled } from '@/modules/multiregion/helpers' -export type MessageType = Notification +export type MessageType = pg.Notification export type ListenerType = (msg: MessageType) => MaybeAsync type ConnectionStateItem = { setupListeners: { @@ -29,16 +28,16 @@ type ConnectionStateItem = { } let shuttingDown = false -let connection: Optional = undefined +let connection: Optional = undefined let redisClient: Optional = undefined let activeReconnect: Optional> = undefined -const connectionState = new WeakMap() +const connectionState = new WeakMap() const listeners: Record = {} const lockName = 'server_postgres_listener_lock' const updateConnectionState = ( - connection: Client, + connection: pg.Client, update: PartialDeep ) => { const state = connectionState.get(connection) || { @@ -101,7 +100,7 @@ async function messageProcessor(msg: MessageType) { } } -const setupListeners = async (connection: Client) => { +const setupListeners = async (connection: pg.Client) => { for (const [key] of Object.entries(listeners)) { const isSetupAlready = !!connectionState.get(connection)?.setupListeners?.[key] dbNotificationLogger.info( @@ -122,7 +121,7 @@ const setupListeners = async (connection: Client) => { } } -const setupConnection = async (connection: Client) => { +const setupConnection = async (connection: pg.Client) => { connection.on('notification', (msg) => { dbNotificationLogger.info({ msg }, 'Message incoming...') void messageProcessor(msg).catch((err) => { @@ -187,7 +186,7 @@ const reconnect = async () => { ) // creating externally managed PG connection from knex mainDB connection settings - const newConnection = new Client({ + const newConnection = new pg.Client({ ...connectionSettings, connectionTimeoutMillis: postgresConnectionCreateTimeoutMillis() }) diff --git a/packages/server/modules/cross-server-sync/index.ts b/packages/server/modules/cross-server-sync/index.ts index 01d3551d3..39072be49 100644 --- a/packages/server/modules/cross-server-sync/index.ts +++ b/packages/server/modules/cross-server-sync/index.ts @@ -189,4 +189,4 @@ const crossServerSyncModule: SpeckleModule = { } } -export = crossServerSyncModule +export default crossServerSyncModule diff --git a/packages/server/modules/cross-server-sync/services/commit.ts b/packages/server/modules/cross-server-sync/services/commit.ts index 47d45d2d3..48508cd2c 100644 --- a/packages/server/modules/cross-server-sync/services/commit.ts +++ b/packages/server/modules/cross-server-sync/services/commit.ts @@ -1,10 +1,10 @@ import fetch from 'cross-fetch' -import { ApolloClient, NormalizedCacheObject, gql } from '@apollo/client/core/core.cjs' +import { ApolloClient, NormalizedCacheObject, gql } from '@apollo/client/core' import { getFrontendOrigin } from '@/modules/shared/helpers/envHelper' import { CreateCommentInput } from '@/test/graphql/generated/graphql' import { Roles, TIME_MS, timeoutAt } from '@speckle/shared' import ObjectLoader from '@speckle/objectloader' -import { noop } from 'lodash' +import { noop } from 'lodash-es' import { crossServerSyncLogger } from '@/observability/logging' import type { SpeckleViewer } from '@speckle/shared' import { retry } from '@speckle/shared' diff --git a/packages/server/modules/cross-server-sync/services/project.ts b/packages/server/modules/cross-server-sync/services/project.ts index bcfd43f18..5b01b14b5 100644 --- a/packages/server/modules/cross-server-sync/services/project.ts +++ b/packages/server/modules/cross-server-sync/services/project.ts @@ -7,7 +7,7 @@ import { assertValidGraphQLResult } from '@/modules/cross-server-sync/utils/graphqlClient' import { CrossSyncProjectMetadataQuery } from '@/modules/cross-server-sync/graph/generated/graphql' -import { omit } from 'lodash' +import { omit } from 'lodash-es' import { getFrontendOrigin } from '@/modules/shared/helpers/envHelper' import { DownloadCommit, diff --git a/packages/server/modules/cross-server-sync/utils/graphqlClient.ts b/packages/server/modules/cross-server-sync/utils/graphqlClient.ts index e344cdf7c..90718ae10 100644 --- a/packages/server/modules/cross-server-sync/utils/graphqlClient.ts +++ b/packages/server/modules/cross-server-sync/utils/graphqlClient.ts @@ -5,7 +5,7 @@ import { HttpLink, gql, ApolloQueryResult -} from '@apollo/client/core/core.cjs' +} from '@apollo/client/core' import { setContext } from '@apollo/client/link/context/context.cjs' import { getServerVersion } from '@/modules/shared/helpers/envHelper' import { CrossSyncClientTestQuery } from '@/modules/cross-server-sync/graph/generated/graphql' diff --git a/packages/server/modules/emails/domain/events.ts b/packages/server/modules/emails/domain/events.ts new file mode 100644 index 000000000..e5f54eac2 --- /dev/null +++ b/packages/server/modules/emails/domain/events.ts @@ -0,0 +1,14 @@ +import type Mail from 'nodemailer/lib/mailer' + +export const emailsEventNamespace = 'emails' as const + +export const EmailsEvents = { + Sent: `${emailsEventNamespace}.sent`, + PreparingToSend: `${emailsEventNamespace}.preparingToSend` +} as const +export type EmailsEvents = (typeof EmailsEvents)[keyof typeof EmailsEvents] + +export type EmailsEventsPayloads = { + [EmailsEvents.Sent]: { options: Mail.Options } + [EmailsEvents.PreparingToSend]: { options: Omit } +} diff --git a/packages/server/modules/emails/graph/resolvers/index.ts b/packages/server/modules/emails/graph/resolvers/index.ts index 848a8ce5e..422c9ed31 100644 --- a/packages/server/modules/emails/graph/resolvers/index.ts +++ b/packages/server/modules/emails/graph/resolvers/index.ts @@ -26,7 +26,7 @@ const requestEmailVerification = requestEmailVerificationFactory({ }) const getUserByEmail = getUserByEmailFactory({ db }) -export = { +export default { User: { async hasPendingVerification(parent) { const email = parent.email diff --git a/packages/server/modules/emails/index.ts b/packages/server/modules/emails/index.ts index 7fca546fe..ad88bd744 100644 --- a/packages/server/modules/emails/index.ts +++ b/packages/server/modules/emails/index.ts @@ -36,7 +36,7 @@ async function sendEmail({ return SendingService.sendEmail({ from, to, subject, text, html }) } -export = { +export default { ...emailsModule, sendEmail } diff --git a/packages/server/modules/emails/migrations/20220118181256-email-verifications.js b/packages/server/modules/emails/migrations/20220118181256-email-verifications.js index d81b9f9d9..01d3a463b 100644 --- a/packages/server/modules/emails/migrations/20220118181256-email-verifications.js +++ b/packages/server/modules/emails/migrations/20220118181256-email-verifications.js @@ -1,7 +1,7 @@ /* istanbul ignore file */ 'use strict' -exports.up = async (knex) => { +const up = async (knex) => { await knex.schema.createTable('email_verifications', (table) => { table.string('id').primary() table.string('email') @@ -11,6 +11,8 @@ exports.up = async (knex) => { }) } -exports.down = async (knex) => { +const down = async (knex) => { await knex.schema.dropTableIfExists('email_verifications') } + +export { up, down } diff --git a/packages/server/modules/emails/rest/index.ts b/packages/server/modules/emails/rest/index.ts index b0d44b050..32b16d4c4 100644 --- a/packages/server/modules/emails/rest/index.ts +++ b/packages/server/modules/emails/rest/index.ts @@ -11,7 +11,7 @@ import { db } from '@/db/knex' import { markUserAsVerifiedFactory } from '@/modules/core/repositories/users' import { withOperationLogging } from '@/observability/domain/businessLogging' -export = (app: Express) => { +export default (app: Express) => { app.get('/auth/verifyemail', async (req, res) => { const logger = req.log try { diff --git a/packages/server/modules/emails/services/sending.ts b/packages/server/modules/emails/services/sending.ts index baf27b40c..4381c911e 100644 --- a/packages/server/modules/emails/services/sending.ts +++ b/packages/server/modules/emails/services/sending.ts @@ -7,8 +7,10 @@ import { getRequestLogger, loggerWithMaybeContext } from '@/observability/utils/requestContext' +import type Mail from 'nodemailer/lib/mailer' +import { getEventBus } from '@/modules/shared/services/eventBus' +import { EmailsEvents } from '@/modules/emails/domain/events' -export type { SendEmailParams } from '@/modules/emails/domain/operations' /** * Send out an e-mail */ @@ -19,21 +21,40 @@ export const sendEmail: SendEmail = async ({ text, html }: SendEmailParams): Promise => { + const eventBus = getEventBus() const logger = getRequestLogger() || loggerWithMaybeContext({ logger: emailLogger }) - const transporter = getTransporter() - if (!transporter) { - logger.warn('No email transport present. Cannot send emails. Skipping send...') - return false - } + try { - const emailFrom = getEmailFromAddress() - await transporter.sendMail({ - from: from || `"Speckle" <${emailFrom}>`, + const baseOptions = { to, subject, text, html + } + + await eventBus.emit({ + eventName: EmailsEvents.PreparingToSend, + payload: { options: baseOptions } }) + + const transporter = getTransporter() + if (!transporter) { + logger.warn('No email transport present. Cannot send emails. Skipping send...') + return false + } + + const emailFrom = getEmailFromAddress() + const options: Mail.Options = { + ...baseOptions, + from: from || `"Speckle" <${emailFrom}>` + } + + await transporter.sendMail(options) + await eventBus.emit({ + eventName: EmailsEvents.Sent, + payload: { options } + }) + const emails = typeof to === 'string' ? [to] : to const distinctIds = await Promise.all( emails.map((email) => resolveMixpanelUserId(email)) @@ -52,3 +73,5 @@ export const sendEmail: SendEmail = async ({ return false } + +export type { SendEmailParams } from '@/modules/emails/domain/operations' diff --git a/packages/server/modules/emails/tests/verifications.spec.ts b/packages/server/modules/emails/tests/verifications.spec.ts index 75b9f24b3..423f22fd3 100644 --- a/packages/server/modules/emails/tests/verifications.spec.ts +++ b/packages/server/modules/emails/tests/verifications.spec.ts @@ -15,7 +15,6 @@ import { import { getEmailVerificationFinalizationRoute } from '@/modules/core/helpers/routeHelper' import { Express } from 'express' import dayjs from 'dayjs' -import { EmailSendingServiceMock } from '@/test/mocks/global' import { createAuthedTestContext, createTestContext, @@ -29,8 +28,8 @@ import { sendEmail } from '@/modules/emails/services/sending' import { renderEmail } from '@/modules/emails/services/emailRendering' import { getUserFactory } from '@/modules/core/repositories/users' import { getServerInfoFactory } from '@/modules/core/repositories/server' +import { createEmailListener, TestEmailListener } from '@/test/speckle-helpers/email' -const mailerMock = EmailSendingServiceMock const getUser = getUserFactory({ db }) const getPendingToken = getPendingTokenFactory({ db }) const deleteVerifications = deleteVerificationsFactory({ db }) @@ -61,25 +60,25 @@ describe('Email verifications @emails', () => { id: '' } + let emailListener: TestEmailListener + before(async () => { await cleanup() await createTestUsers([userA, userB]) + emailListener = await createEmailListener() }) after(async () => { await cleanup() + await emailListener.destroy() }) afterEach(async () => { - mailerMock.resetMockedFunctions() + emailListener.reset() }) it('sends out 1 verification email immediately after new account creation', async () => { - const sendEmailInvocations = mailerMock.hijackFunction( - 'sendEmail', - async () => true, - { times: 2 } - ) + const { getSends } = emailListener.listen({ times: 2 }) const newGuy: BasicTestUser = { name: 'happy to be here', @@ -90,7 +89,8 @@ describe('Email verifications @emails', () => { await createTestUser(newGuy) - const emailParams = sendEmailInvocations.args[0][0] + const sentEmails = getSends() + const emailParams = sentEmails[0] expect(emailParams).to.be.ok expect(emailParams.subject).to.contain('Speckle account email verification') @@ -98,7 +98,7 @@ describe('Email verifications @emails', () => { expect(verification).to.be.ok // There should be only 1 email! - expect(sendEmailInvocations.args.length).to.eq(1) + expect(sentEmails.length).to.eq(1) }) describe('when authenticated', () => { @@ -146,16 +146,16 @@ describe('Email verifications @emails', () => { // delete previous requests for userA, if any await deleteVerifications(userA.email) - const sendEmailInvocations = mailerMock.hijackFunction( - 'sendEmail', - async () => false - ) + const { getSends } = emailListener.listen({ times: 2 }) const result = await invokeRequestVerification(userA) expect(result).to.not.haveGraphQLErrors() expect(result.data?.requestVerification).to.be.true - const emailParams = sendEmailInvocations.args[0][0] + const sentEmails = getSends() + expect(sentEmails.length).to.eq(1) + + const emailParams = sentEmails[0] expect(emailParams).to.be.ok expect(emailParams.subject).to.contain('Speckle account email verification') expect(emailParams.html).to.be.ok diff --git a/packages/server/modules/fileuploads/graph/resolvers/fileUploads.ts b/packages/server/modules/fileuploads/graph/resolvers/fileUploads.ts index 77d4199f7..22a72a871 100644 --- a/packages/server/modules/fileuploads/graph/resolvers/fileUploads.ts +++ b/packages/server/modules/fileuploads/graph/resolvers/fileUploads.ts @@ -247,7 +247,7 @@ const fileUploadMutations: Resolvers['FileUploadMutations'] = { } } -export = { +export default { Stream: { async fileUploads(parent) { const projectDb = await getProjectDbClient({ projectId: parent.id }) diff --git a/packages/server/modules/fileuploads/migrations/20210915130000-fileuploads.js b/packages/server/modules/fileuploads/migrations/20210915130000-fileuploads.js index 9f02a06a8..9078fa829 100644 --- a/packages/server/modules/fileuploads/migrations/20210915130000-fileuploads.js +++ b/packages/server/modules/fileuploads/migrations/20210915130000-fileuploads.js @@ -1,7 +1,7 @@ /* istanbul ignore file */ 'use strict' -exports.up = async (knex) => { +const up = async (knex) => { await knex.schema.createTable('file_uploads', (table) => { table.string('id').primary() table.string('streamId', 10).references('id').inTable('streams').onDelete('cascade') @@ -22,6 +22,8 @@ exports.up = async (knex) => { }) } -exports.down = async (knex) => { +const down = async (knex) => { await knex.schema.dropTableIfExists('file_uploads') } + +export { up, down } diff --git a/packages/server/modules/fileuploads/queues/fileimports.ts b/packages/server/modules/fileuploads/queues/fileimports.ts index 3429766c7..945156313 100644 --- a/packages/server/modules/fileuploads/queues/fileimports.ts +++ b/packages/server/modules/fileuploads/queues/fileimports.ts @@ -10,7 +10,7 @@ import { } from '@/modules/shared/helpers/envHelper' import { Logger, fileUploadsLogger as logger } from '@/observability/logging' import { TIME, TIME_MS } from '@speckle/shared' -import { initializeQueue as setupQueue } from '@speckle/shared/dist/commonjs/queue/index.js' +import { initializeQueue as setupQueue } from '@speckle/shared/queue' import { JobPayload } from '@speckle/shared/workers/fileimport' import { FileImportQueue } from '@/modules/fileuploads/domain/types' import Bull, { diff --git a/packages/server/modules/fileuploads/repositories/fileUploads.ts b/packages/server/modules/fileuploads/repositories/fileUploads.ts index 6839dabf2..a1a323211 100644 --- a/packages/server/modules/fileuploads/repositories/fileUploads.ts +++ b/packages/server/modules/fileuploads/repositories/fileUploads.ts @@ -21,7 +21,7 @@ import { import { Knex } from 'knex' import { FileImportJobNotFoundError } from '@/modules/fileuploads/helpers/errors' import { compositeCursorTools } from '@/modules/shared/helpers/dbHelper' -import { clamp } from 'lodash' +import { clamp } from 'lodash-es' const tables = { fileUploads: (db: Knex) => db(FileUploads.name) diff --git a/packages/server/modules/fileuploads/services/presigned.ts b/packages/server/modules/fileuploads/services/presigned.ts index 8a11f831d..2cfa22c35 100644 --- a/packages/server/modules/fileuploads/services/presigned.ts +++ b/packages/server/modules/fileuploads/services/presigned.ts @@ -9,7 +9,7 @@ import { import { ModelNotFoundError } from '@/modules/core/errors/model' import { ensureError } from '@speckle/shared' import { FileImportJobNotFoundError } from '@/modules/fileuploads/helpers/errors' -import { get } from 'lodash' +import { get } from 'lodash-es' export const registerUploadCompleteAndStartFileImportFactory = (deps: { registerCompletedUpload: RegisterCompletedUpload diff --git a/packages/server/modules/fileuploads/tests/e2e/presigned.graph.spec.ts b/packages/server/modules/fileuploads/tests/e2e/presigned.graph.spec.ts index f2514852c..07c16ce30 100644 --- a/packages/server/modules/fileuploads/tests/e2e/presigned.graph.spec.ts +++ b/packages/server/modules/fileuploads/tests/e2e/presigned.graph.spec.ts @@ -4,7 +4,7 @@ import { beforeEachContext } from '@/test/hooks' import { createProject, grantProjectPermissions } from '@/test/projectHelper' import { BasicTestBranch, createTestBranch } from '@/test/speckle-helpers/branchHelper' import { Nullable, Optional, Roles, ServerRoles, StreamRoles } from '@speckle/shared' -import { put } from 'axios' +import axios from 'axios' import { expect } from 'chai' import cryptoRandomString from 'crypto-random-string' import gql from 'graphql-tag' @@ -133,7 +133,7 @@ const startFileImport = async (params: TestContext) => { fileId = uploadDetails.data.fileUploadMutations.generateUploadUrl.fileId - const putResult = await put( + const putResult = await axios.put( uploadDetails.data.fileUploadMutations.generateUploadUrl.url, cryptoRandomString({ length: 100 }) //test content ) diff --git a/packages/server/modules/fileuploads/tests/helpers/creation.ts b/packages/server/modules/fileuploads/tests/helpers/creation.ts index b9f1f2267..e8ebfb821 100644 --- a/packages/server/modules/fileuploads/tests/helpers/creation.ts +++ b/packages/server/modules/fileuploads/tests/helpers/creation.ts @@ -3,7 +3,7 @@ import cryptoRandomString from 'crypto-random-string' import { saveUploadFileFactory } from '@/modules/fileuploads/repositories/fileUploads' import { db } from '@/db/knex' import { FileImportMessage } from '@/modules/fileuploads/domain/operations' -import { assign } from 'lodash' +import { assign } from 'lodash-es' import { FileUploadRecord, FileUploadRecordV2 diff --git a/packages/server/modules/fileuploads/tests/integration/fileuploads.spec.ts b/packages/server/modules/fileuploads/tests/integration/fileuploads.spec.ts index 9d9bfee20..73c56be9f 100644 --- a/packages/server/modules/fileuploads/tests/integration/fileuploads.spec.ts +++ b/packages/server/modules/fileuploads/tests/integration/fileuploads.spec.ts @@ -8,6 +8,7 @@ import cryptoRandomString from 'crypto-random-string' import { noErrors } from '@/test/helpers' import { TIME_MS } from '@speckle/shared' import { initUploadTestEnvironment } from '@/modules/fileuploads/tests/helpers/init' +import { fileURLToPath } from 'url' const { createStream, createUser, createToken } = initUploadTestEnvironment() const gqlQueryToListFileUploads = `query ($streamId: String!) { @@ -76,11 +77,12 @@ describe('FileUploads @fileuploads integration', () => { describe('Uploads files', () => { it('Should upload a single file', async () => { + const readmePath = fileURLToPath(import.meta.resolve('@/readme.md')) const response = await request(app) .post(`/api/file/autodetect/${createdStreamId}/main`) .set('Authorization', `Bearer ${userOneToken}`) .set('Accept', 'application/json') - .attach('test.ifc', require.resolve('@/readme.md'), 'test.ifc') + .attach('test.ifc', readmePath, 'test.ifc') expect(response.statusCode).to.equal(201) expect(response.headers['content-type']).to.contain('application/json;') @@ -104,8 +106,12 @@ describe('FileUploads @fileuploads integration', () => { const response = await request(app) .post(`/api/file/autodetect/${createdStreamId}/main`) .set('Authorization', `Bearer ${userOneToken}`) - .attach('blob1', require.resolve('@/readme.md'), 'test1.ifc') - .attach('blob2', require.resolve('@/package.json'), 'test2.ifc') + .attach('blob1', fileURLToPath(import.meta.resolve('@/readme.md')), 'test1.ifc') + .attach( + 'blob2', + fileURLToPath(import.meta.resolve('@/package.json')), + 'test2.ifc' + ) expect(response.status).to.equal(201) expect(response.headers['content-type']).to.contain('application/json;') expect(response.body.uploadResults).to.have.lengthOf(2) @@ -213,7 +219,11 @@ describe('FileUploads @fileuploads integration', () => { .post(`/api/file/autodetect/${createdStreamId}/main`) .set('Authorization', `Bearer ${badToken}`) .set('Accept', 'application/json') - .attach('test.ifc', require.resolve('@/readme.md'), 'test.ifc') + .attach( + 'test.ifc', + fileURLToPath(import.meta.resolve('@/readme.md')), + 'test.ifc' + ) expect(response.statusCode).to.equal(403) const gqlResponse = await sendRequest(userOneToken, { query: gqlQueryToListFileUploads, @@ -233,7 +243,11 @@ describe('FileUploads @fileuploads integration', () => { .post(`/api/file/autodetect/${badStreamId}/main`) .set('Authorization', `Bearer ${userOneToken}`) .set('Accept', 'application/json') - .attach('test.ifc', require.resolve('@/readme.md'), 'test.ifc') + .attach( + 'test.ifc', + fileURLToPath(import.meta.resolve('@/readme.md')), + 'test.ifc' + ) 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: gqlQueryToListFileUploads, @@ -260,7 +274,11 @@ describe('FileUploads @fileuploads integration', () => { .post(`/api/file/autodetect/${streamTwoId}/main`) .set('Authorization', `Bearer ${userOneToken}`) .set('Accept', 'application/json') - .attach('test.ifc', require.resolve('@/readme.md'), 'test.ifc') + .attach( + 'test.ifc', + fileURLToPath(import.meta.resolve('@/readme.md')), + 'test.ifc' + ) expect(response.statusCode).to.equal(403) expect(response.body).to.deep.equal({ diff --git a/packages/server/modules/fileuploads/tests/integration/presigned.integration.spec.ts b/packages/server/modules/fileuploads/tests/integration/presigned.integration.spec.ts index 1a0977cfe..f6e3e4966 100644 --- a/packages/server/modules/fileuploads/tests/integration/presigned.integration.spec.ts +++ b/packages/server/modules/fileuploads/tests/integration/presigned.integration.spec.ts @@ -24,7 +24,7 @@ import { Knex } from 'knex' import cryptoRandomString from 'crypto-random-string' import { expect } from 'chai' import { testLogger } from '@/observability/logging' -import { put } from 'axios' +import axios from 'axios' import { expectToThrow } from '@/test/assertionHelper' import { AlreadyRegisteredBlobError, @@ -177,7 +177,7 @@ const { FF_LARGE_FILE_IMPORTS_ENABLED, FF_NEXT_GEN_FILE_IMPORTER_ENABLED } = urlExpiryDurationSeconds: expiryDuration }) - const response = await put(url, cryptoRandomString({ length: fileSize })) + const response = await axios.put(url, cryptoRandomString({ length: fileSize })) expect( response.status, JSON.stringify({ statusText: response.statusText, body: response.data }) @@ -238,7 +238,7 @@ const { FF_LARGE_FILE_IMPORTS_ENABLED, FF_NEXT_GEN_FILE_IMPORTER_ENABLED } = urlExpiryDurationSeconds: expiryDuration }) - const response = await put(url, 'test content') // more than 1 byte long + const response = await axios.put(url, 'test content') // more than 1 byte long expect( response.status, JSON.stringify({ statusText: response.statusText, body: response.data }) @@ -281,7 +281,7 @@ const { FF_LARGE_FILE_IMPORTS_ENABLED, FF_NEXT_GEN_FILE_IMPORTER_ENABLED } = urlExpiryDurationSeconds: expiryDuration }) - const response = await put(url, cryptoRandomString({ length: fileSize })) + const response = await axios.put(url, cryptoRandomString({ length: fileSize })) expect( response.status, JSON.stringify({ statusText: response.statusText, body: response.data }) @@ -330,7 +330,7 @@ const { FF_LARGE_FILE_IMPORTS_ENABLED, FF_NEXT_GEN_FILE_IMPORTER_ENABLED } = urlExpiryDurationSeconds: expiryDuration }) - const response = await put(url, cryptoRandomString({ length: fileSize })) + const response = await axios.put(url, cryptoRandomString({ length: fileSize })) expect( response.status, JSON.stringify({ statusText: response.statusText, body: response.data }) @@ -379,7 +379,7 @@ const { FF_LARGE_FILE_IMPORTS_ENABLED, FF_NEXT_GEN_FILE_IMPORTER_ENABLED } = urlExpiryDurationSeconds: expiryDuration }) - const response = await put(url, cryptoRandomString({ length: fileSize })) + const response = await axios.put(url, cryptoRandomString({ length: fileSize })) expect( response.status, JSON.stringify({ statusText: response.statusText, body: response.data }) diff --git a/packages/server/modules/fileuploads/tests/unit/fileuploads.spec.ts b/packages/server/modules/fileuploads/tests/unit/fileuploads.spec.ts index fffa802d2..6e4ba2892 100644 --- a/packages/server/modules/fileuploads/tests/unit/fileuploads.spec.ts +++ b/packages/server/modules/fileuploads/tests/unit/fileuploads.spec.ts @@ -17,7 +17,7 @@ import { FileUploadConvertedStatus } from '@/modules/fileuploads/helpers/types' import { TIME } from '@speckle/shared' import { initUploadTestEnvironment } from '@/modules/fileuploads/tests/helpers/init' import { pushJobToFileImporterFactory } from '@/modules/fileuploads/services/createFileImport' -import { assign, get } from 'lodash' +import { assign, get } from 'lodash-es' import { buildFileUploadMessage } from '@/modules/fileuploads/tests/helpers/creation' import { getFeatureFlags } from '@speckle/shared/environment' import { JobPayload } from '@speckle/shared/workers/fileimport' diff --git a/packages/server/modules/gatekeeper/clients/checkout/createCheckoutSession.ts b/packages/server/modules/gatekeeper/clients/checkout/createCheckoutSession.ts index 79cdcaa68..01eca8372 100644 --- a/packages/server/modules/gatekeeper/clients/checkout/createCheckoutSession.ts +++ b/packages/server/modules/gatekeeper/clients/checkout/createCheckoutSession.ts @@ -2,6 +2,7 @@ import { getResultUrl } from '@/modules/gatekeeper/clients/getResultUrl' import { CreateCheckoutSession, + GetStripeClient, GetWorkspacePlanPriceId } from '@/modules/gatekeeper/domain/billing' import { EnvironmentResourceError } from '@/modules/shared/errors' @@ -9,11 +10,11 @@ import { Stripe } from 'stripe' export const createCheckoutSessionFactory = ({ - stripe, + getStripeClient, frontendOrigin, getWorkspacePlanPrice }: { - stripe: Stripe + getStripeClient: GetStripeClient frontendOrigin: string getWorkspacePlanPrice: GetWorkspacePlanPriceId }): CreateCheckoutSession => @@ -37,7 +38,7 @@ export const createCheckoutSessionFactory = ? `${frontendOrigin}/workspaces/actions/create?workspaceId=${workspaceId}&payment_status=canceled&session_id={CHECKOUT_SESSION_ID}` : `${resultUrl.toString()}&payment_status=canceled&session_id={CHECKOUT_SESSION_ID}` - const session = await stripe.checkout.sessions.create({ + const session = await getStripeClient().checkout.sessions.create({ mode: 'subscription', line_items: costLineItems, diff --git a/packages/server/modules/gatekeeper/clients/stripe.ts b/packages/server/modules/gatekeeper/clients/stripe.ts index a799f06c6..644d25703 100644 --- a/packages/server/modules/gatekeeper/clients/stripe.ts +++ b/packages/server/modules/gatekeeper/clients/stripe.ts @@ -2,24 +2,39 @@ import { getResultUrl } from '@/modules/gatekeeper/clients/getResultUrl' import { GetRecurringPrices, + GetStripeClient, GetSubscriptionData, ReconcileSubscriptionData, SubscriptionData } from '@/modules/gatekeeper/domain/billing' -import { LogicError } from '@/modules/shared/errors' +import { LogicError, TestOnlyLogicError } from '@/modules/shared/errors' +import { getStripeApiKey, isTestEnv } from '@/modules/shared/helpers/envHelper' import { TIME_MS } from '@speckle/shared' -import { isString } from 'lodash' +import { isString } from 'lodash-es' import { Stripe } from 'stripe' +let stripeClient: Stripe | undefined = undefined + +export const getStripeClient: GetStripeClient = () => { + if (!stripeClient) stripeClient = new Stripe(getStripeApiKey(), { typescript: true }) + return stripeClient +} + +export const setStripeClient = (client: Stripe | undefined) => { + if (!isTestEnv()) { + throw new TestOnlyLogicError() + } + + stripeClient = client +} + export const createCustomerPortalUrlFactory = ({ - stripe, + getStripeClient, frontendOrigin - }: // getWorkspacePlanPrice - { - stripe: Stripe + }: { + getStripeClient: GetStripeClient frontendOrigin: string - // getWorkspacePlanPrice: GetWorkspacePlanPrice }) => async ({ workspaceId, @@ -30,7 +45,7 @@ export const createCustomerPortalUrlFactory = workspaceId: string workspaceSlug: string }): Promise => { - const session = await stripe.billingPortal.sessions.create({ + const session = await getStripeClient().billingPortal.sessions.create({ customer: customerId, return_url: getResultUrl({ frontendOrigin, @@ -42,15 +57,11 @@ export const createCustomerPortalUrlFactory = } export const getStripeSubscriptionDataFactory = - ({ - stripe - }: // getWorkspacePlanPrice - { - stripe: Stripe - // getWorkspacePlanPrice: GetWorkspacePlanPrice - }): GetSubscriptionData => + ({ getStripeClient }: { getStripeClient: GetStripeClient }): GetSubscriptionData => async ({ subscriptionId }) => { - const stripeSubscription = await stripe.subscriptions.retrieve(subscriptionId) + const stripeSubscription = await getStripeClient().subscriptions.retrieve( + subscriptionId + ) return parseSubscriptionData(stripeSubscription) } @@ -93,10 +104,10 @@ export const parseSubscriptionData = ( // on each change, we're reconciling that state to stripe export const reconcileWorkspaceSubscriptionFactory = ({ - stripe, + getStripeClient, getStripeSubscriptionData }: { - stripe: Stripe + getStripeClient: GetStripeClient getStripeSubscriptionData: GetSubscriptionData }): ReconcileSubscriptionData => async ({ subscriptionData, prorationBehavior }) => { @@ -133,16 +144,16 @@ export const reconcileWorkspaceSubscriptionFactory = // workspaceSubscription.subscriptionData.products. // const item = workspaceSubscription.subscriptionData.products.find(p => p.) - await stripe.subscriptions.update(subscriptionData.subscriptionId, { + await getStripeClient().subscriptions.update(subscriptionData.subscriptionId, { items, proration_behavior: prorationBehavior }) } export const getRecurringPricesFactory = - (deps: { stripe: Stripe }): GetRecurringPrices => + (deps: { getStripeClient: GetStripeClient }): GetRecurringPrices => async () => { - const results = await deps.stripe.prices.list({ + const results = await deps.getStripeClient().prices.list({ type: 'recurring', limit: 100, active: true diff --git a/packages/server/modules/gatekeeper/domain/billing.ts b/packages/server/modules/gatekeeper/domain/billing.ts index 3bff8f4e8..5e3126bdf 100644 --- a/packages/server/modules/gatekeeper/domain/billing.ts +++ b/packages/server/modules/gatekeeper/domain/billing.ts @@ -18,12 +18,13 @@ import { WorkspacePlan, WorkspacePlanBillingIntervals } from '@speckle/shared' +import type Stripe from 'stripe' import { OverrideProperties } from 'type-fest' import { z } from 'zod' export { Currency } from '@/modules/gatekeeperCore/domain/billing' -export { WorkspaceSeat, WorkspaceSeatType } -export { +export { type WorkspaceSeat, WorkspaceSeatType } +export type { GetWorkspaceRoleAndSeat, GetWorkspaceRolesAndSeats } from '@/modules/workspacesCore/domain/operations' @@ -189,6 +190,8 @@ export type GetWorkspaceSubscriptionBySubscriptionId = (args: { subscriptionId: string }) => Promise +export type GetStripeClient = () => Stripe + export type GetSubscriptionData = (args: { subscriptionId: string }) => Promise diff --git a/packages/server/modules/gatekeeper/events/eventListener.ts b/packages/server/modules/gatekeeper/events/eventListener.ts index 87e92e0a9..9e1dfd186 100644 --- a/packages/server/modules/gatekeeper/events/eventListener.ts +++ b/packages/server/modules/gatekeeper/events/eventListener.ts @@ -12,14 +12,14 @@ import { addWorkspaceSubscriptionSeatIfNeededFactory } from '@/modules/gatekeepe import { getWorkspacePlanPriceId, getWorkspacePlanProductId -} from '@/modules/gatekeeper/stripe' +} from '@/modules/gatekeeper/helpers/prices' import { getEventBus } from '@/modules/shared/services/eventBus' import { WorkspaceEvents } from '@/modules/workspacesCore/domain/events' import { Knex } from 'knex' -import Stripe from 'stripe' +import { GetStripeClient } from '@/modules/gatekeeper/domain/billing' export const initializeEventListenersFactory = - ({ db, stripe }: { db: Knex; stripe: Stripe }) => + ({ db, getStripeClient }: { db: Knex; getStripeClient: GetStripeClient }) => () => { const eventBus = getEventBus() const quitCbs = [ @@ -31,8 +31,10 @@ export const initializeEventListenersFactory = getWorkspacePlanPriceId, getWorkspacePlanProductId, reconcileSubscriptionData: reconcileWorkspaceSubscriptionFactory({ - stripe, - getStripeSubscriptionData: getStripeSubscriptionDataFactory({ stripe }) + getStripeClient, + getStripeSubscriptionData: getStripeSubscriptionDataFactory({ + getStripeClient + }) }), upsertWorkspaceSubscription: upsertWorkspaceSubscriptionFactory({ db }), countSeatsByTypeInWorkspace: countSeatsByTypeInWorkspaceFactory({ db }) diff --git a/packages/server/modules/gatekeeper/graph/resolvers/index.ts b/packages/server/modules/gatekeeper/graph/resolvers/index.ts index 163cfd1b8..e6697a77f 100644 --- a/packages/server/modules/gatekeeper/graph/resolvers/index.ts +++ b/packages/server/modules/gatekeeper/graph/resolvers/index.ts @@ -17,15 +17,15 @@ import { db } from '@/db/knex' import { createCustomerPortalUrlFactory, getRecurringPricesFactory, + getStripeClient, getStripeSubscriptionDataFactory, reconcileWorkspaceSubscriptionFactory } from '@/modules/gatekeeper/clients/stripe' import { getWorkspacePlanPriceId, - getStripeClient, getWorkspacePlanProductId, getWorkspacePlanProductAndPriceIds -} from '@/modules/gatekeeper/stripe' +} from '@/modules/gatekeeper/helpers/prices' import { deleteCheckoutSessionFactory, getWorkspaceCheckoutSessionFactory, @@ -67,7 +67,7 @@ const { FF_GATEKEEPER_MODULE_ENABLED, FF_BILLING_INTEGRATION_ENABLED } = const getWorkspacePlan = getWorkspacePlanFactory({ db }) -export = FF_GATEKEEPER_MODULE_ENABLED +export default FF_GATEKEEPER_MODULE_ENABLED ? ({ Workspace: { plan: async (parent) => { @@ -117,7 +117,7 @@ export = FF_GATEKEEPER_MODULE_ENABLED 'This cannot be, if there is a sub, there is a workspace' ) return await createCustomerPortalUrlFactory({ - stripe: getStripeClient(), + getStripeClient, frontendOrigin: getFrontendOrigin() })({ workspaceId: workspaceSubscription.workspaceId, @@ -143,7 +143,7 @@ export = FF_GATEKEEPER_MODULE_ENABLED planPrices: async (parent) => { const getWorkspacePlanPrices = getWorkspacePlanProductPricesFactory({ getRecurringPrices: getRecurringPricesFactory({ - stripe: getStripeClient() + getStripeClient }), getWorkspacePlanProductAndPriceIds }) @@ -311,7 +311,7 @@ export = FF_GATEKEEPER_MODULE_ENABLED planPrices: async () => { const getWorkspacePlanPrices = getWorkspacePlanProductPricesFactory({ getRecurringPrices: getRecurringPricesFactory({ - stripe: getStripeClient() + getStripeClient }), getWorkspacePlanProductAndPriceIds }) @@ -410,7 +410,7 @@ export = FF_GATEKEEPER_MODULE_ENABLED ) const createCheckoutSession = createCheckoutSessionFactory({ - stripe: getStripeClient(), + getStripeClient, frontendOrigin: getFrontendOrigin(), getWorkspacePlanPrice: getWorkspacePlanPriceId }) @@ -454,13 +454,14 @@ export = FF_GATEKEEPER_MODULE_ENABLED Roles.Workspace.Admin, ctx.resourceAccessRules ) - const stripe = getStripeClient() const upgradeWorkspaceSubscription = upgradeWorkspaceSubscriptionFactory({ getWorkspacePlan: getWorkspacePlanFactory({ db }), reconcileSubscriptionData: reconcileWorkspaceSubscriptionFactory({ - stripe, - getStripeSubscriptionData: getStripeSubscriptionDataFactory({ stripe }) + getStripeClient, + getStripeSubscriptionData: getStripeSubscriptionDataFactory({ + getStripeClient + }) }), countSeatsByTypeInWorkspace: countSeatsByTypeInWorkspaceFactory({ db diff --git a/packages/server/modules/gatekeeper/stripe.ts b/packages/server/modules/gatekeeper/helpers/prices.ts similarity index 90% rename from packages/server/modules/gatekeeper/stripe.ts rename to packages/server/modules/gatekeeper/helpers/prices.ts index 37ce2df59..62c989fdc 100644 --- a/packages/server/modules/gatekeeper/stripe.ts +++ b/packages/server/modules/gatekeeper/helpers/prices.ts @@ -3,17 +3,9 @@ import { GetWorkspacePlanProductAndPriceIds, GetWorkspacePlanProductId } from '@/modules/gatekeeper/domain/billing' -import { getStringFromEnv, getStripeApiKey } from '@/modules/shared/helpers/envHelper' -import { Stripe } from 'stripe' +import { getStringFromEnv } from '@/modules/shared/helpers/envHelper' import { NotImplementedError } from '@/modules/shared/errors' -let stripeClient: Stripe | undefined = undefined - -export const getStripeClient = () => { - if (!stripeClient) stripeClient = new Stripe(getStripeApiKey(), { typescript: true }) - return stripeClient -} - const loadProductAndPriceIds: GetWorkspacePlanProductAndPriceIds = () => ({ team: { productId: getStringFromEnv('WORKSPACE_TEAM_SEAT_STRIPE_PRODUCT_ID'), diff --git a/packages/server/modules/gatekeeper/index.ts b/packages/server/modules/gatekeeper/index.ts index 9b7cd22e5..480ba28aa 100644 --- a/packages/server/modules/gatekeeper/index.ts +++ b/packages/server/modules/gatekeeper/index.ts @@ -9,10 +9,9 @@ import { db } from '@/db/knex' import { gatekeeperScopes } from '@/modules/gatekeeper/scopes' import { initializeEventListenersFactory } from '@/modules/gatekeeper/events/eventListener' import { - getStripeClient, getWorkspacePlanProductAndPriceIds, getWorkspacePlanProductId -} from '@/modules/gatekeeper/stripe' +} from '@/modules/gatekeeper/helpers/prices' import { scheduleExecutionFactory } from '@/modules/core/services/taskScheduler' import { acquireTaskLockFactory, @@ -25,6 +24,7 @@ import { upsertWorkspaceSubscriptionFactory } from '@/modules/gatekeeper/repositories/billing' import { + getStripeClient, getStripeSubscriptionDataFactory, reconcileWorkspaceSubscriptionFactory } from '@/modules/gatekeeper/clients/stripe' @@ -52,14 +52,15 @@ const scheduleWorkspaceSubscriptionDownscale = ({ }: { scheduleExecution: ScheduleExecution }) => { - const stripe = getStripeClient() - const getStripeSubscriptionData = getStripeSubscriptionDataFactory({ stripe }) + const getStripeSubscriptionData = getStripeSubscriptionDataFactory({ + getStripeClient + }) const manageSubscriptionDownscale = manageSubscriptionDownscaleFactory({ downscaleWorkspaceSubscription: downscaleWorkspaceSubscriptionFactory({ countSeatsByTypeInWorkspace: countSeatsByTypeInWorkspaceFactory({ db }), getWorkspacePlan: getWorkspacePlanFactory({ db }), reconcileSubscriptionData: reconcileWorkspaceSubscriptionFactory({ - stripe, + getStripeClient, getStripeSubscriptionData }), getWorkspacePlanProductId @@ -117,7 +118,7 @@ const gatekeeperModule: SpeckleModule = { quitListeners = initializeEventListenersFactory({ db, - stripe: getStripeClient() + getStripeClient })() const isLicenseValid = await validateModuleLicense({ @@ -152,4 +153,4 @@ async function isProjectReadOnly({ projectId }: { projectId: string }) { if (readOnly) throw new WorkspaceReadOnlyError() } -export = gatekeeperModule +export default gatekeeperModule diff --git a/packages/server/modules/gatekeeper/repositories/billing.ts b/packages/server/modules/gatekeeper/repositories/billing.ts index 4164505e4..c379b7f41 100644 --- a/packages/server/modules/gatekeeper/repositories/billing.ts +++ b/packages/server/modules/gatekeeper/repositories/billing.ts @@ -27,7 +27,7 @@ import { Workspace } from '@/modules/workspacesCore/domain/types' import { Workspaces } from '@/modules/workspacesCore/helpers/db' import { PaidWorkspacePlans, WorkspacePlan } from '@speckle/shared' import { Knex } from 'knex' -import { omit } from 'lodash' +import { omit } from 'lodash-es' const WorkspacePlans = buildTableHelper('workspace_plans', [ 'workspaceId', diff --git a/packages/server/modules/gatekeeper/rest/billing.ts b/packages/server/modules/gatekeeper/rest/billing.ts index 5cc57e3cd..3dd572d3f 100644 --- a/packages/server/modules/gatekeeper/rest/billing.ts +++ b/packages/server/modules/gatekeeper/rest/billing.ts @@ -5,6 +5,7 @@ import { getStripeEndpointSigningKey } from '@/modules/shared/helpers/envHelper' import { db } from '@/db/knex' import { completeCheckoutSessionFactory } from '@/modules/gatekeeper/services/checkout' import { + getStripeClient, getStripeSubscriptionDataFactory, parseSubscriptionData } from '@/modules/gatekeeper/clients/stripe' @@ -19,9 +20,8 @@ import { getWorkspaceSubscriptionFactory } from '@/modules/gatekeeper/repositories/billing' import { WorkspaceAlreadyPaidError } from '@/modules/gatekeeper/errors/billing' -import { getStripeClient } from '@/modules/gatekeeper/stripe' import { handleSubscriptionUpdateFactory } from '@/modules/gatekeeper/services/subscriptions' -import { SubscriptionData } from '@/modules/gatekeeper/domain/billing' +import { GetStripeClient, SubscriptionData } from '@/modules/gatekeeper/domain/billing' import { extendLoggerComponent } from '@/observability/logging' import { OperationName, @@ -133,7 +133,7 @@ export const getBillingRouter = (): Router => { getWorkspacePlan: getWorkspacePlanFactory({ db }), getWorkspaceSubscription: getWorkspaceSubscriptionFactory({ db }), getSubscriptionData: getStripeSubscriptionDataFactory({ - stripe + getStripeClient }), emitEvent: emit }) @@ -215,9 +215,9 @@ export const getBillingRouter = (): Router => { ) break case 'invoice.created': - const subscriptionData = await getSubscriptionFromEventFactory({ stripe })( - event - ) + const subscriptionData = await getSubscriptionFromEventFactory({ + getStripeClient + })(event) if (!subscriptionData) break await withOperationLogging( async () => @@ -249,14 +249,14 @@ export const getBillingRouter = (): Router => { } const getSubscriptionFromEventFactory = - ({ stripe }: { stripe: Stripe }) => + ({ getStripeClient }: { getStripeClient: GetStripeClient }) => async (event: Stripe.InvoiceCreatedEvent): Promise => { const subscription = event.data.object.subscription if (!subscription) { return null } if (typeof subscription === 'string') { - return await getStripeSubscriptionDataFactory({ stripe })({ + return await getStripeSubscriptionDataFactory({ getStripeClient })({ subscriptionId: subscription }) } diff --git a/packages/server/modules/gatekeeper/services/checkout.ts b/packages/server/modules/gatekeeper/services/checkout.ts index a0359b2a7..4620a88b2 100644 --- a/packages/server/modules/gatekeeper/services/checkout.ts +++ b/packages/server/modules/gatekeeper/services/checkout.ts @@ -14,7 +14,7 @@ import { } from '@/modules/gatekeeper/errors/billing' import { throwUncoveredError } from '@speckle/shared' import { EventBusEmit } from '@/modules/shared/services/eventBus' -import { GetWorkspacePlan } from '@speckle/shared/dist/commonjs/authz/domain/workspaces/operations.js' +import { GetWorkspacePlan } from '@speckle/shared/authz' import { GatekeeperEvents } from '@/modules/gatekeeperCore/domain/events' export const completeCheckoutSessionFactory = diff --git a/packages/server/modules/gatekeeper/services/prices.spec.ts b/packages/server/modules/gatekeeper/services/prices.spec.ts index ff780a5e1..238d5ff83 100644 --- a/packages/server/modules/gatekeeper/services/prices.spec.ts +++ b/packages/server/modules/gatekeeper/services/prices.spec.ts @@ -7,7 +7,7 @@ import { expectToThrow } from '@/test/assertionHelper' import { mockRedisCacheProviderFactory } from '@/test/redisHelper' import { PaidWorkspacePlans, WorkspacePlanBillingIntervals } from '@speckle/shared' import { expect } from 'chai' -import { flatten } from 'lodash' +import { flatten } from 'lodash-es' import { WorkspacePlanProductAndPriceIds } from '@/modules/gatekeeper/domain/billing' const testProductAndPriceIds: WorkspacePlanProductAndPriceIds = { diff --git a/packages/server/modules/gatekeeper/services/prices.ts b/packages/server/modules/gatekeeper/services/prices.ts index a451bcb15..da44fd4a5 100644 --- a/packages/server/modules/gatekeeper/services/prices.ts +++ b/packages/server/modules/gatekeeper/services/prices.ts @@ -17,7 +17,7 @@ import { TIME_MS, WorkspacePlanBillingIntervals } from '@speckle/shared' -import { set } from 'lodash' +import { set } from 'lodash-es' export const getFreshWorkspacePlanProductPricesFactory = (deps: { diff --git a/packages/server/modules/gatekeeper/services/subscriptions.ts b/packages/server/modules/gatekeeper/services/subscriptions.ts index ca5129316..876d8a421 100644 --- a/packages/server/modules/gatekeeper/services/subscriptions.ts +++ b/packages/server/modules/gatekeeper/services/subscriptions.ts @@ -23,7 +23,7 @@ import { throwUncoveredError, WorkspacePlans } from '@speckle/shared' -import { cloneDeep, isEqual, omit } from 'lodash' +import { cloneDeep, isEqual, omit } from 'lodash-es' import { CountSeatsByTypeInWorkspace } from '@/modules/gatekeeper/domain/operations' import { EventBusEmit } from '@/modules/shared/services/eventBus' import { GatekeeperEvents } from '@/modules/gatekeeperCore/domain/events' diff --git a/packages/server/modules/gatekeeper/services/subscriptions/manageSubscriptionDownscale.ts b/packages/server/modules/gatekeeper/services/subscriptions/manageSubscriptionDownscale.ts index 7c1dd5f2a..aff83e268 100644 --- a/packages/server/modules/gatekeeper/services/subscriptions/manageSubscriptionDownscale.ts +++ b/packages/server/modules/gatekeeper/services/subscriptions/manageSubscriptionDownscale.ts @@ -16,7 +16,7 @@ import { import { mutateSubscriptionDataWithNewValidSeatNumbers } from '@/modules/gatekeeper/services/subscriptions/mutateSubscriptionDataWithNewValidSeatNumbers' import { Logger } from '@/observability/logging' import { throwUncoveredError, WorkspacePlans } from '@speckle/shared' -import { cloneDeep, isEqual } from 'lodash' +import { cloneDeep, isEqual } from 'lodash-es' type DownscaleWorkspaceSubscription = (args: { workspaceSubscription: WorkspaceSubscription diff --git a/packages/server/modules/gatekeeper/services/subscriptions/upgradeWorkspaceSubscription.ts b/packages/server/modules/gatekeeper/services/subscriptions/upgradeWorkspaceSubscription.ts index f6895b512..d5f0f2686 100644 --- a/packages/server/modules/gatekeeper/services/subscriptions/upgradeWorkspaceSubscription.ts +++ b/packages/server/modules/gatekeeper/services/subscriptions/upgradeWorkspaceSubscription.ts @@ -28,7 +28,7 @@ import { WorkspacePlanBillingIntervals, WorkspacePlans } from '@speckle/shared' -import { cloneDeep } from 'lodash' +import { cloneDeep } from 'lodash-es' export const upgradeWorkspaceSubscriptionFactory = ({ diff --git a/packages/server/modules/gatekeeper/tests/helpers.ts b/packages/server/modules/gatekeeper/tests/helpers.ts index 07b1a5fbd..41607cc54 100644 --- a/packages/server/modules/gatekeeper/tests/helpers.ts +++ b/packages/server/modules/gatekeeper/tests/helpers.ts @@ -3,7 +3,7 @@ import { WorkspaceSubscription } from '@/modules/gatekeeper/domain/billing' import cryptoRandomString from 'crypto-random-string' -import { assign } from 'lodash' +import { assign } from 'lodash-es' export const createTestSubscriptionData = ( overrides: Partial = {} diff --git a/packages/server/modules/gatekeeper/tests/helpers/workspacePlan.ts b/packages/server/modules/gatekeeper/tests/helpers/workspacePlan.ts index 3a7ddd33e..582205c03 100644 --- a/packages/server/modules/gatekeeper/tests/helpers/workspacePlan.ts +++ b/packages/server/modules/gatekeeper/tests/helpers/workspacePlan.ts @@ -1,6 +1,6 @@ import { WorkspacePlan } from '@speckle/shared' import cryptoRandomString from 'crypto-random-string' -import { assign } from 'lodash' +import { assign } from 'lodash-es' import { SubscriptionData, SubscriptionProduct, diff --git a/packages/server/modules/gatekeeper/tests/integration/billingRepositories.spec.ts b/packages/server/modules/gatekeeper/tests/integration/billingRepositories.spec.ts index f83b3f8f6..e440b92bf 100644 --- a/packages/server/modules/gatekeeper/tests/integration/billingRepositories.spec.ts +++ b/packages/server/modules/gatekeeper/tests/integration/billingRepositories.spec.ts @@ -24,7 +24,6 @@ import { createAndStoreTestWorkspaceFactory } from '@/test/speckle-helpers/works import { PaidWorkspacePlans } from '@speckle/shared' import { expect } from 'chai' import cryptoRandomString from 'crypto-random-string' -import { beforeEach } from 'mocha' const upsertWorkspace = upsertWorkspaceFactory({ db }) const createAndStoreTestWorkspace = createAndStoreTestWorkspaceFactory({ diff --git a/packages/server/modules/gatekeeper/tests/unit/checkout.spec.ts b/packages/server/modules/gatekeeper/tests/unit/checkout.spec.ts index 4d375dacf..8929b7b0d 100644 --- a/packages/server/modules/gatekeeper/tests/unit/checkout.spec.ts +++ b/packages/server/modules/gatekeeper/tests/unit/checkout.spec.ts @@ -12,7 +12,7 @@ import { SubscriptionData, WorkspaceSubscription } from '@/modules/gatekeeper/domain/billing' -import { omit } from 'lodash' +import { omit } from 'lodash-es' import { PaidWorkspacePlan, PaidWorkspacePlans, diff --git a/packages/server/modules/gatekeeper/tests/unit/stripe.spec.ts b/packages/server/modules/gatekeeper/tests/unit/stripe.spec.ts index 3fb59a018..a31e52bc5 100644 --- a/packages/server/modules/gatekeeper/tests/unit/stripe.spec.ts +++ b/packages/server/modules/gatekeeper/tests/unit/stripe.spec.ts @@ -24,7 +24,7 @@ describe('Stripe integration', () => { ] }) const reconcileWorkspaceSubscription = reconcileWorkspaceSubscriptionFactory({ - stripe: fakeStripe, + getStripeClient: () => fakeStripe, getStripeSubscriptionData: async () => subscriptionData }) @@ -59,7 +59,7 @@ describe('Stripe integration', () => { ] }) const reconcileWorkspaceSubscription = reconcileWorkspaceSubscriptionFactory({ - stripe: fakeStripe, + getStripeClient: () => fakeStripe, getStripeSubscriptionData: async () => buildTestSubscriptionData({ subscriptionId, @@ -107,7 +107,7 @@ describe('Stripe integration', () => { ] }) const reconcileWorkspaceSubscription = reconcileWorkspaceSubscriptionFactory({ - stripe: fakeStripe, + getStripeClient: () => fakeStripe, getStripeSubscriptionData: async () => buildTestSubscriptionData({ subscriptionId, diff --git a/packages/server/modules/gatekeeper/tests/unit/subscriptions.spec.ts b/packages/server/modules/gatekeeper/tests/unit/subscriptions.spec.ts index 3534f42a6..02a9ff5f2 100644 --- a/packages/server/modules/gatekeeper/tests/unit/subscriptions.spec.ts +++ b/packages/server/modules/gatekeeper/tests/unit/subscriptions.spec.ts @@ -26,7 +26,7 @@ import { expectToThrow } from '@/test/assertionHelper' import { PaidWorkspacePlans, throwUncoveredError, WorkspacePlan } from '@speckle/shared' import { expect } from 'chai' import cryptoRandomString from 'crypto-random-string' -import { omit } from 'lodash' +import { omit } from 'lodash-es' import { upgradeWorkspaceSubscriptionFactory } from '@/modules/gatekeeper/services/subscriptions/upgradeWorkspaceSubscription' import { EventBusEmit } from '@/modules/shared/services/eventBus' import { testLogger } from '@/observability/logging' diff --git a/packages/server/modules/gatekeeper/tests/unit/workspacePlans.spec.ts b/packages/server/modules/gatekeeper/tests/unit/workspacePlans.spec.ts index bb6321963..4fbfeaabd 100644 --- a/packages/server/modules/gatekeeper/tests/unit/workspacePlans.spec.ts +++ b/packages/server/modules/gatekeeper/tests/unit/workspacePlans.spec.ts @@ -13,7 +13,7 @@ import { } from '@speckle/shared' import { expect } from 'chai' import cryptoRandomString from 'crypto-random-string' -import { omit } from 'lodash' +import { omit } from 'lodash-es' import { buildTestWorkspacePlan, buildTestWorkspaceSubscription diff --git a/packages/server/modules/gendo/graph/resolvers/index.ts b/packages/server/modules/gendo/graph/resolvers/index.ts index 9515a53f8..8ee98e447 100644 --- a/packages/server/modules/gendo/graph/resolvers/index.ts +++ b/packages/server/modules/gendo/graph/resolvers/index.ts @@ -50,7 +50,7 @@ const throwIfRateLimited = throwIfRateLimitedFactory({ rateLimiterEnabled: isRateLimiterEnabled() }) -export = FF_GENDOAI_MODULE_ENABLED +export default FF_GENDOAI_MODULE_ENABLED ? ({ Version: { async gendoAIRenders(parent) { diff --git a/packages/server/modules/gendo/index.ts b/packages/server/modules/gendo/index.ts index 4c26d858f..52dff0d1f 100644 --- a/packages/server/modules/gendo/index.ts +++ b/packages/server/modules/gendo/index.ts @@ -5,7 +5,7 @@ import restApi from '@/modules/gendo/rest/index' const { FF_GENDOAI_MODULE_ENABLED } = getFeatureFlags() -export = { +export default { async init({ app }) { if (!FF_GENDOAI_MODULE_ENABLED) return moduleLogger.info('🪞 Init Gendo AI render module') diff --git a/packages/server/modules/gendo/repositories/index.ts b/packages/server/modules/gendo/repositories/index.ts index 1a1cee585..5505ea78a 100644 --- a/packages/server/modules/gendo/repositories/index.ts +++ b/packages/server/modules/gendo/repositories/index.ts @@ -11,7 +11,7 @@ import { import { UserCredits } from '@/modules/gendo/domain/types' import { GendoAIRenderRecord } from '@/modules/gendo/helpers/types' import { Knex } from 'knex' -import { pick } from 'lodash' +import { pick } from 'lodash-es' const tables = { gendoAIRenders: (db: Knex) => db(GendoAIRenders.name), diff --git a/packages/server/modules/index.ts b/packages/server/modules/index.ts index ce44c26ac..361d4aa97 100644 --- a/packages/server/modules/index.ts +++ b/packages/server/modules/index.ts @@ -4,7 +4,15 @@ import fs from 'fs' import path from 'path' import { appRoot, packageRoot } from '@/bootstrap' -import { values, merge, camelCase, reduce, intersection, difference, set } from 'lodash' +import { + values, + merge, + camelCase, + reduce, + intersection, + difference, + set +} from 'lodash-es' import baseTypeDefs from '@/modules/core/graph/schema/baseTypeDefs' import { scalarResolvers } from '@/modules/core/graph/scalars' import { makeExecutableSchema } from '@graphql-tools/schema' @@ -53,7 +61,7 @@ const loadedModules: SpeckleModule[] = [] */ let hasInitializationOccurred = false -function autoloadFromDirectory(dirPath: string) { +async function autoloadFromDirectory(dirPath: string) { if (!fs.existsSync(dirPath)) return const results: Record = {} @@ -65,7 +73,7 @@ function autoloadFromDirectory(dirPath: string) { const ext = path.extname(file) if (['.js', '.ts'].includes(ext)) { const name = camelCase(path.basename(file, ext)) - results[name] = require(pathToFile) + results[name] = await import(pathToFile) } } } @@ -117,7 +125,7 @@ async function getSpeckleModules() { const moduleNames = getEnabledModuleNames() for (const dir of moduleNames) { - const moduleIndex = require(`./${dir}/index`) + const moduleIndex = await import(`./${dir}/index`) // CJS/ESM interop is weird let moduleDefinition: SpeckleModule @@ -179,26 +187,28 @@ export const shutdown = async () => { /** * Autoloads dataloaders from all modules */ -export const graphDataloadersBuilders = (): RequestDataLoadersBuilder[] => { +export const graphDataloadersBuilders = async (): Promise< + RequestDataLoadersBuilder[] +> => { let dataLoaders: RequestDataLoadersBuilder[] = [] const enabledModuleNames = getEnabledModuleNames() // load code modules from /modules const codeModuleDirs = fs.readdirSync(`${appRoot}/modules`) - codeModuleDirs.forEach((file) => { - if (!enabledModuleNames.includes(file)) return + for (const file of codeModuleDirs) { + if (!enabledModuleNames.includes(file)) continue const modulePath = path.join(`${appRoot}/modules`, file) // load dataloaders const fullPath = path.join(modulePath, 'graph', 'dataloaders') if (fs.existsSync(fullPath)) { - const newLoaders = values(autoloadFromDirectory(fullPath)) + const newLoaders = values(await autoloadFromDirectory(fullPath)) .map((l) => l.default) .filter(isNonNullable) dataLoaders = [...dataLoaders, ...newLoaders] } - }) + } return dataLoaders } @@ -207,10 +217,12 @@ export const graphDataloadersBuilders = (): RequestDataLoadersBuilder[] => * GQL components - typedefs, resolvers, directives * (assets & directives will be loaded from even disabled components cause the schema must be static) */ -const graphComponents = (): Pick, 'resolvers'> & { - directiveBuilders: Record - typeDefs: string[] -} => { +const graphComponents = async (): Promise< + Pick, 'resolvers'> & { + directiveBuilders: Record + typeDefs: string[] + } +> => { const enabledModuleNames = getEnabledModuleNames() // Base query and mutation to allow for type extension by modules. @@ -233,15 +245,15 @@ const graphComponents = (): Pick, 'resolvers'> & { // load code modules from /modules const codeModuleDirs = fs.readdirSync(`${appRoot}/modules`) - codeModuleDirs.forEach((file) => { + for (const file of codeModuleDirs) { const isEnabledModule = enabledModuleNames.includes(file) const fullPath = path.join(`${appRoot}/modules`, file) // first pass load of resolvers const resolversPath = path.join(fullPath, 'graph', 'resolvers') if (isEnabledModule && fs.existsSync(resolversPath)) { - const newResolverObjs = values(autoloadFromDirectory(resolversPath)).map((o) => - 'default' in o ? o.default : o + const newResolverObjs = values(await autoloadFromDirectory(resolversPath)).map( + (o) => ('default' in o ? o.default : o) ) resolverObjs = [...resolverObjs, ...newResolverObjs] } @@ -252,7 +264,7 @@ const graphComponents = (): Pick, 'resolvers'> & { directiveBuilders = { ...directiveBuilders, ...reduce( - values(autoloadFromDirectory(directivesPath)), + values(await autoloadFromDirectory(directivesPath)), (acc, directivesObj) => { return { ...acc, ...directivesObj } }, @@ -260,7 +272,7 @@ const graphComponents = (): Pick, 'resolvers'> & { ) } } - }) + } const resolvers = { ...scalarResolvers } resolverObjs.forEach((o) => { @@ -270,8 +282,8 @@ const graphComponents = (): Pick, 'resolvers'> & { return { resolvers, typeDefs, directiveBuilders } } -export const graphSchema = (mocksConfig?: AppMocksConfig) => { - const { resolvers, typeDefs, directiveBuilders } = graphComponents() +export const graphSchema = async (mocksConfig?: AppMocksConfig) => { + const { resolvers, typeDefs, directiveBuilders } = await graphComponents() const directiveTypedefs: string[] = [] const directiveSchemaTransformers: SchemaTransformer[] = [] @@ -311,9 +323,9 @@ export const graphSchema = (mocksConfig?: AppMocksConfig) => { /** * Load GQL mock configs from speckle modules */ -export const moduleMockConfigs = ( +export const moduleMockConfigs = async ( moduleWhitelist: string[] -): Record => { +): Promise> => { const enabledModuleNames = intersection(getEnabledModuleNames(), moduleWhitelist) // Config default exports keyed by module name @@ -322,15 +334,15 @@ export const moduleMockConfigs = ( // load code modules from /modules const codeModuleDirs = fs.readdirSync(`${appRoot}/modules`) - codeModuleDirs.forEach((moduleName) => { + for (const moduleName of codeModuleDirs) { const fullPath = path.join(`${appRoot}/modules`, moduleName) - if (!enabledModuleNames.includes(moduleName)) return + if (!enabledModuleNames.includes(moduleName)) continue // load mock config const mocksFolderPath = path.join(fullPath, 'graph', 'mocks') if (fs.existsSync(mocksFolderPath)) { // We only take the first mocks.ts file we find (for now) - const mainConfig = values(autoloadFromDirectory(mocksFolderPath)) + const mainConfig = values(await autoloadFromDirectory(mocksFolderPath)) .map((l) => l.default) .filter(isNonNullable)[0] @@ -338,7 +350,7 @@ export const moduleMockConfigs = ( mockConfigs[moduleName] = mainConfig } } - }) + } return mockConfigs } @@ -363,7 +375,9 @@ export const moduleAuthLoaders = async (params: { if (!fs.existsSync(loadersFolderPath)) continue // We only take the first loaders.ts file we find (for now) - const moduleLoadersBuilderFn = values(autoloadFromDirectory(loadersFolderPath)) + const moduleLoadersBuilderFn = values( + await autoloadFromDirectory(loadersFolderPath) + ) .map((l) => l.default) .filter(isNonNullable)[0] as Optional> diff --git a/packages/server/modules/mocks.ts b/packages/server/modules/mocks.ts index db655aa06..07b6e41a2 100644 --- a/packages/server/modules/mocks.ts +++ b/packages/server/modules/mocks.ts @@ -4,7 +4,7 @@ import { isProdEnv, isTestEnv } from '@/modules/shared/helpers/envHelper' -import { has, reduce } from 'lodash' +import { has, reduce } from 'lodash-es' import { IMockStore, IMocks } from '@graphql-tools/mock' import { moduleMockConfigs } from '@/modules/index' @@ -21,7 +21,7 @@ import { Streams } from '@/modules/core/dbSchema' */ const buildBaseConfig = async (): Promise => { // Async import so that we only import this when envs actually have mocks on - const { faker } = require('@faker-js/faker') as typeof import('@faker-js/faker') + const { faker } = await import('@faker-js/faker') return { resolvers: ({ helpers: { getFieldValue }, store }) => ({ @@ -96,7 +96,7 @@ export async function buildMocksConfig(): Promise<{ return { mocks: false, mockEntireSchema: false } } - const configs = moduleMockConfigs(mockableModuleList) + const configs = await moduleMockConfigs(mockableModuleList) if (!Object.keys(configs).length) { return { mocks: false, mockEntireSchema: false } } diff --git a/packages/server/modules/multiregion/regionConfig.ts b/packages/server/modules/multiregion/regionConfig.ts index 1eea4faed..27227c8d5 100644 --- a/packages/server/modules/multiregion/regionConfig.ts +++ b/packages/server/modules/multiregion/regionConfig.ts @@ -4,7 +4,8 @@ import path from 'node:path' import { getMultiRegionConfigPath, - isDevOrTestEnv + isDevOrTestEnv, + isTestEnv } from '@/modules/shared/helpers/envHelper' import { type Optional } from '@speckle/shared' import { isMultiRegionEnabled } from '@/modules/multiregion/helpers' @@ -13,25 +14,30 @@ import { MultiRegionConfig, loadMultiRegionsConfig } from '@speckle/shared/environment/db' +import { TestOnlyLogicError } from '@/modules/shared/errors' +import { PartialDeep } from 'type-fest' +import { merge } from 'lodash-es' let multiRegionConfig: Optional = undefined -const getMultiRegionConfig = async (): Promise => { +const emptyConfig = (): MultiRegionConfig => ({ + main: { + postgres: { connectionUri: '' }, + blobStorage: { + accessKey: '', + secretKey: '', + endpoint: '', + s3Region: '', + bucket: '', + createBucketIfNotExists: true + } + }, + regions: {} +}) + +export const getMultiRegionConfig = async (): Promise => { // Only for non region enabled dev envs - const emptyReturn = (): MultiRegionConfig => ({ - main: { - postgres: { connectionUri: '' }, - blobStorage: { - accessKey: '', - secretKey: '', - endpoint: '', - s3Region: '', - bucket: '', - createBucketIfNotExists: true - } - }, - regions: {} - }) + const emptyReturn = () => emptyConfig() if (!multiRegionConfig) { const relativePath = getMultiRegionConfigPath({ unsafe: isDevOrTestEnv() }) @@ -68,3 +74,13 @@ export const getDefaultProjectRegionKey = async (): Promise => { const defaultRegionKey = (await getMultiRegionConfig()).defaultProjectRegionKey return defaultRegionKey ?? null } + +export const setMultiRegionConfig = ( + config: Optional> +) => { + if (!isTestEnv()) { + throw new TestOnlyLogicError() + } + + multiRegionConfig = config ? merge({}, emptyConfig(), config) : undefined +} diff --git a/packages/server/modules/multiregion/repositories/index.ts b/packages/server/modules/multiregion/repositories/index.ts index aa8e61dd0..53e81768c 100644 --- a/packages/server/modules/multiregion/repositories/index.ts +++ b/packages/server/modules/multiregion/repositories/index.ts @@ -7,7 +7,7 @@ import { } from '@/modules/multiregion/domain/operations' import { RegionRecord } from '@/modules/multiregion/helpers/types' import { Knex } from 'knex' -import { pick } from 'lodash' +import { pick } from 'lodash-es' export const Regions = buildTableHelper('regions', [ 'key', diff --git a/packages/server/modules/multiregion/services/queue.ts b/packages/server/modules/multiregion/services/queue.ts index 277de80e5..befa3400d 100644 --- a/packages/server/modules/multiregion/services/queue.ts +++ b/packages/server/modules/multiregion/services/queue.ts @@ -56,7 +56,7 @@ import { import { withTransaction } from '@/modules/shared/helpers/dbHelper' import { getRedisUrl } from '@/modules/shared/helpers/envHelper' import { waitForRegionProjectFactory } from '@/modules/core/services/projects' -import { chunk } from 'lodash' +import { chunk } from 'lodash-es' import { getStreamCollaboratorsFactory } from '@/modules/core/repositories/streams' const MULTIREGION_QUEUE_NAME = isTestEnv() diff --git a/packages/server/modules/multiregion/tests/e2e/serverAdmin.graph.spec.ts b/packages/server/modules/multiregion/tests/e2e/serverAdmin.graph.spec.ts index bdce57304..cbd976840 100644 --- a/packages/server/modules/multiregion/tests/e2e/serverAdmin.graph.spec.ts +++ b/packages/server/modules/multiregion/tests/e2e/serverAdmin.graph.spec.ts @@ -1,6 +1,11 @@ -import { ObjectStorage } from '@/modules/blobstorage/clients/objectStorage' +import { mainDb } from '@/db/knex' +import { getMainObjectStorage } from '@/modules/blobstorage/clients/objectStorage' import { DataRegionsConfig } from '@/modules/multiregion/domain/types' import { isMultiRegionEnabled } from '@/modules/multiregion/helpers' +import { + getMultiRegionConfig, + setMultiRegionConfig +} from '@/modules/multiregion/regionConfig' import { BasicTestUser, createTestUser } from '@/test/authHelper' import { CreateNewRegionDocument, @@ -16,14 +21,15 @@ import { TestApolloServer } from '@/test/graphqlHelper' import { beforeEachContext, getRegionKeys } from '@/test/hooks' -import { - MultiRegionBlobStorageSelectorMock, - MultiRegionConfigMock, - MultiRegionDbSelectorMock -} from '@/test/mocks/global' + import { truncateRegionsSafely } from '@/test/speckle-helpers/regions' import { Roles } from '@speckle/shared' +import { + getConnectionSettings, + MultiRegionConfig +} from '@speckle/shared/environment/db' import { expect } from 'chai' +import { merge } from 'lodash-es' const isEnabled = isMultiRegionEnabled() @@ -65,19 +71,37 @@ isEnabled } } + let originalConfig: MultiRegionConfig + before(async () => { - MultiRegionConfigMock.mockFunction( - 'getAvailableRegionConfig', - async () => fakeRegionConfig - ) - MultiRegionDbSelectorMock.mockFunction('initializeRegion', async () => - Promise.resolve() - ) - MultiRegionBlobStorageSelectorMock.mockFunction('initializeRegion', async () => - Promise.resolve( - undefined as unknown as { private: ObjectStorage; public: ObjectStorage } - ) - ) + // Faking multi region config (but retain active config, in case were running multiregion tests) + originalConfig = await getMultiRegionConfig() + + const connectionUri = getConnectionSettings(mainDb).connectionString! + const mainStorage = getMainObjectStorage() + + const regionConfig = { + postgres: { + connectionUri, + skipInitialization: true + }, + blobStorage: { + accessKey: mainStorage.params.credentials.accessKeyId, + secretKey: mainStorage.params.credentials.secretAccessKey, + s3Region: mainStorage.params.region, + bucket: mainStorage.params.bucket, + endpoint: mainStorage.params.endpoint, + createBucketIfNotExists: false + } + } + const regionsConfig = { + regions: { + [fakeRegionKey1]: regionConfig, + [fakeRegionKey2]: regionConfig + } + } + + setMultiRegionConfig(merge({}, originalConfig, regionsConfig)) await beforeEachContext() testAdminUser = await createTestUser({ role: Roles.Server.Admin }) @@ -85,10 +109,9 @@ isEnabled apollo = await testApolloServer({ authUserId: testAdminUser.id }) }) - after(() => { - MultiRegionConfigMock.resetMockedFunctions() - MultiRegionDbSelectorMock.resetMockedFunctions() - MultiRegionBlobStorageSelectorMock.resetMockedFunctions() + after(async () => { + setMultiRegionConfig(originalConfig) + await truncateRegionsSafely() }) describe('server config', () => { diff --git a/packages/server/modules/multiregion/utils/dbSelector.ts b/packages/server/modules/multiregion/utils/dbSelector.ts index 5327035f1..3c0cb74a2 100644 --- a/packages/server/modules/multiregion/utils/dbSelector.ts +++ b/packages/server/modules/multiregion/utils/dbSelector.ts @@ -25,7 +25,7 @@ import { getProjectRegionKey, getRegisteredRegionConfigs } from '@/modules/multiregion/utils/regionSelector' -import { get, mapValues } from 'lodash' +import { get, mapValues } from 'lodash-es' import { isMultiRegionEnabled } from '@/modules/multiregion/helpers' let getter: GetProjectDb | undefined = undefined @@ -166,27 +166,34 @@ export const initializeRegion: InitializeRegion = async ({ regionKey }) => { ) const newRegionConfig = regionConfigs[regionKey] + const newRegionDbConfig = newRegionConfig.postgres + const regionDb = configureClient(newRegionConfig) - await migrateDbToLatest({ db: regionDb.public, region: regionKey }) - const mainDbConfig = await getMainRegionConfig() - const mainDb = configureClient(mainDbConfig) + if (!newRegionDbConfig.skipInitialization) { + await migrateDbToLatest({ db: regionDb.public, region: regionKey }) - const sslmode = newRegionConfig.postgres.publicTlsCertificate ? 'require' : 'disable' + const mainDbConfig = await getMainRegionConfig() + const mainDb = configureClient(mainDbConfig) - await setUpUserReplication({ - from: mainDb, - to: regionDb, - regionName: regionKey, - sslmode - }) + const sslmode = newRegionConfig.postgres.publicTlsCertificate + ? 'require' + : 'disable' - await setUpProjectReplication({ - from: regionDb, - to: mainDb, - regionName: regionKey, - sslmode - }) + await setUpUserReplication({ + from: mainDb, + to: regionDb, + regionName: regionKey, + sslmode + }) + + await setUpProjectReplication({ + from: regionDb, + to: mainDb, + regionName: regionKey, + sslmode + }) + } // pushing to the singleton object here, only if its not available // if this is being triggered from init, its gonna be set after anyway diff --git a/packages/server/modules/notifications/domain/events.ts b/packages/server/modules/notifications/domain/events.ts new file mode 100644 index 000000000..caa9e6916 --- /dev/null +++ b/packages/server/modules/notifications/domain/events.ts @@ -0,0 +1,16 @@ +import { NotificationMessage } from '@/modules/notifications/helpers/types' + +export const notificationsEventNamespace = 'notifications' as const + +export const NotificationsEvents = { + Received: `${notificationsEventNamespace}.received` +} as const + +export type NotificationsEvents = + (typeof NotificationsEvents)[keyof typeof NotificationsEvents] + +export type NotificationsEventsPayloads = { + [NotificationsEvents.Received]: { + message: NotificationMessage + } +} diff --git a/packages/server/modules/notifications/graph/resolvers/userNotificationPreferences.ts b/packages/server/modules/notifications/graph/resolvers/userNotificationPreferences.ts index 39f9bb59a..9973a6a55 100644 --- a/packages/server/modules/notifications/graph/resolvers/userNotificationPreferences.ts +++ b/packages/server/modules/notifications/graph/resolvers/userNotificationPreferences.ts @@ -20,7 +20,7 @@ const updateNotificationPreferences = updateNotificationPreferencesFactory({ saveUserNotificationPreferences: saveUserNotificationPreferencesFactory({ db }) }) -export = { +export default { User: { async notificationPreferences(parent) { const preferences = await getUserNotificationPreferences(parent.id) diff --git a/packages/server/modules/notifications/helpers/types.ts b/packages/server/modules/notifications/helpers/types.ts index 78abb52d2..e4675713b 100644 --- a/packages/server/modules/notifications/helpers/types.ts +++ b/packages/server/modules/notifications/helpers/types.ts @@ -2,7 +2,7 @@ import { StreamAccessRequestRecord } from '@/modules/accessrequests/repositories' import { MaybeAsync, Optional } from '@/modules/shared/helpers/typeHelper' import { Job } from 'bull' -import { isObject, has } from 'lodash' +import { isObject, has } from 'lodash-es' import { Logger } from 'pino' export enum NotificationType { diff --git a/packages/server/modules/notifications/services/handlers/activityDigest.ts b/packages/server/modules/notifications/services/handlers/activityDigest.ts index 198f056d7..66be7915b 100644 --- a/packages/server/modules/notifications/services/handlers/activityDigest.ts +++ b/packages/server/modules/notifications/services/handlers/activityDigest.ts @@ -10,7 +10,7 @@ import { } from '@/modules/activitystream/helpers/types' import { ServerInfo, UserRecord } from '@/modules/core/helpers/types' import { sendEmail, SendEmailParams } from '@/modules/emails/services/sending' -import { groupBy } from 'lodash' +import { groupBy } from 'lodash-es' import { packageRoot } from '@/bootstrap' import path from 'path' import * as ejs from 'ejs' diff --git a/packages/server/modules/notifications/services/queue.ts b/packages/server/modules/notifications/services/queue.ts index 87fde2288..0890188a1 100644 --- a/packages/server/modules/notifications/services/queue.ts +++ b/packages/server/modules/notifications/services/queue.ts @@ -19,6 +19,8 @@ import cryptoRandomString from 'crypto-random-string' import { logger, notificationsLogger, Observability } from '@/observability/logging' import { ensureErrorOrWrapAsCause } from '@/modules/shared/errors/ensureError' import { TIME_MS } from '@speckle/shared' +import { getEventBus } from '@/modules/shared/services/eventBus' +import { NotificationsEvents } from '@/modules/notifications/domain/events' export type NotificationJobResult = { status: NotificationJobResultsStatus @@ -119,6 +121,8 @@ export function registerNotificationHandlers( */ export async function consumeIncomingNotifications() { const queue = getQueue() + const eventBus = getEventBus() + void queue.process(async (job): Promise => { let notificationType: Optional try { @@ -139,6 +143,13 @@ export async function consumeIncomingNotifications() { const type = typedPayload.type as NotificationType notificationType = type + await eventBus.emit({ + eventName: NotificationsEvents.Received, + payload: { + message: typedPayload + } + }) + const handler = handlers.get(type) if (!handler) { throw new UnhandledNotificationError(null, { info: { payload, type } }) diff --git a/packages/server/modules/notifications/tests/activityDigest.spec.ts b/packages/server/modules/notifications/tests/activityDigest.spec.ts index 5674ce43f..480ba2991 100644 --- a/packages/server/modules/notifications/tests/activityDigest.spec.ts +++ b/packages/server/modules/notifications/tests/activityDigest.spec.ts @@ -27,7 +27,7 @@ import { prepareSummaryEmailFactory } from '@/modules/notifications/services/handlers/activityDigest' import { expect } from 'chai' -import { range } from 'lodash' +import { range } from 'lodash-es' const prepareSummaryEmail = prepareSummaryEmailFactory({ renderEmail diff --git a/packages/server/modules/notifications/tests/notifications.spec.ts b/packages/server/modules/notifications/tests/notifications.spec.ts index cd6d49785..1aa881509 100644 --- a/packages/server/modules/notifications/tests/notifications.spec.ts +++ b/packages/server/modules/notifications/tests/notifications.spec.ts @@ -1,7 +1,5 @@ -import { mockRequireModule } from '@/test/mockHelper' import { MentionedInCommentData, - MentionedInCommentMessage, NotificationType } from '@/modules/notifications/helpers/types' import { publishNotification } from '@/modules/notifications/services/publication' @@ -10,7 +8,6 @@ import { NotificationsStateManager, purgeNotifications } from '@/test/notificationsHelper' -import { Optional } from '@/modules/shared/helpers/typeHelper' import { expect } from 'chai' import { InvalidNotificationError, @@ -18,10 +15,8 @@ import { UnhandledNotificationError } from '@/modules/notifications/errors' import { NotificationJobResultsStatus } from '@/modules/notifications/services/queue' - -const mentionsHandlerMock = mockRequireModule< - typeof import('@/modules/notifications/services/handlers/mentionedInComment') ->(['@/modules/notifications/services/handlers/mentionedInComment']) +import { getEventBus } from '@/modules/shared/services/eventBus' +import { NotificationsEvents } from '@/modules/notifications/domain/events' describe('Notifications', () => { let notificationsState: NotificationsStateManager @@ -36,8 +31,7 @@ describe('Notifications', () => { }) afterEach(() => { - mentionsHandlerMock.resetMockedFunctions() - mentionsHandlerMock.disable() + notificationsState.reset() }) it('can be emitted and routed to proper handler on consumption', async () => { @@ -49,13 +43,6 @@ describe('Notifications', () => { streamId: 'ddd' } - let enqueuedMessage: Optional - - mentionsHandlerMock.enable() - mentionsHandlerMock.mockFunction('default', async (msg) => { - enqueuedMessage = msg - }) - // Enqueue notification const msgId = await publishNotification(NotificationType.MentionedInComment, { targetUserId, @@ -65,6 +52,7 @@ describe('Notifications', () => { // Wait for ack await notificationsState.waitForMsgAck(msgId) + const enqueuedMessage = notificationsState.collectedMessages().at(-1)! expect(enqueuedMessage).to.be.ok expect(enqueuedMessage?.targetUserId).to.eq(targetUserId) expect(enqueuedMessage?.type).to.eq(NotificationType.MentionedInComment) @@ -118,8 +106,7 @@ describe('Notifications', () => { streamId: 'ddd' } - mentionsHandlerMock.enable() - mentionsHandlerMock.mockFunction('default', async () => { + getEventBus().listenOnce(NotificationsEvents.Received, () => { throw error }) diff --git a/packages/server/modules/previews/migrations/20210426200000-previews.js b/packages/server/modules/previews/migrations/20210426200000-previews.js index e623d8a9a..043ace23b 100644 --- a/packages/server/modules/previews/migrations/20210426200000-previews.js +++ b/packages/server/modules/previews/migrations/20210426200000-previews.js @@ -1,7 +1,7 @@ /* istanbul ignore file */ 'use strict' -exports.up = async (knex) => { +const up = async (knex) => { await knex.schema.createTable('object_preview', (table) => { table.string('streamId', 10).references('id').inTable('streams').onDelete('cascade') table.string('objectId').notNullable() @@ -19,7 +19,9 @@ exports.up = async (knex) => { }) } -exports.down = async (knex) => { +const down = async (knex) => { await knex.schema.dropTableIfExists('object_preview') await knex.schema.dropTableIfExists('previews') } + +export { up, down } diff --git a/packages/server/modules/previews/ogImage.ts b/packages/server/modules/previews/ogImage.ts index 1b7b1aaa6..5cec5565a 100644 --- a/packages/server/modules/previews/ogImage.ts +++ b/packages/server/modules/previews/ogImage.ts @@ -1,6 +1,7 @@ import sharp from 'sharp' import xmlescape from 'xml-escape' import pixelWidth from 'string-pixel-width' +import { fileURLToPath } from 'url' type SharpInput = | Buffer @@ -37,7 +38,9 @@ export async function makeOgImage( } const logo = await sharp( - require.resolve('#/assets/previews/images/speckle_logo_and_text.png') + fileURLToPath( + import.meta.resolve('#/assets/previews/images/speckle_logo_and_text.png') + ) ) .resize({ height: panelHeight }) .toBuffer() diff --git a/packages/server/modules/previews/rest/router.ts b/packages/server/modules/previews/rest/router.ts index ea2002154..aa732534c 100644 --- a/packages/server/modules/previews/rest/router.ts +++ b/packages/server/modules/previews/rest/router.ts @@ -45,11 +45,15 @@ import { import { requestObjectPreviewFactory } from '@/modules/previews/queues/previews' import type { Queue } from 'bull' import type { Knex } from 'knex' +import { fileURLToPath } from 'url' const httpErrorImage = (httpErrorCode: number) => - require.resolve(`#/assets/previews/images/preview_${httpErrorCode}.png`) + fileURLToPath( + import.meta.resolve(`#/assets/previews/images/preview_${httpErrorCode}.png`) + ) -const noPreviewImage = require.resolve('#/assets/previews/images/no_preview.png') +const noPreviewImage = () => + fileURLToPath(import.meta.resolve('#/assets/previews/images/no_preview.png')) const buildCreateObjectPreviewFunction = ({ projectDb, @@ -103,7 +107,7 @@ export const previewRouterFactory = ({ const { hasPermissions, httpErrorCode } = await checkStreamPermissions(req) if (!hasPermissions) { // return res.status( httpErrorCode ).end() - return res.sendFile(httpErrorImage(httpErrorCode)) + return res.sendFile(await httpErrorImage(httpErrorCode)) } const getCommitsByStreamId = legacyGetPaginatedStreamCommitsPageFactory({ @@ -117,7 +121,7 @@ export const previewRouterFactory = ({ cursor: undefined }) if (!commits || commits.length === 0) { - return res.sendFile(noPreviewImage) + return res.sendFile(await noPreviewImage()) } const lastCommit = commits[0] const getObjectPreviewBufferOrFilepath = getObjectPreviewBufferOrFilepathFactory({ @@ -162,7 +166,7 @@ export const previewRouterFactory = ({ const { hasPermissions, httpErrorCode } = await checkStreamPermissions(req) if (!hasPermissions) { // return res.status( httpErrorCode ).end() - return res.sendFile(httpErrorImage(httpErrorCode)) + return res.sendFile(await httpErrorImage(httpErrorCode)) } const projectDb = await getProjectDbClient({ projectId: req.params.streamId }) @@ -186,7 +190,7 @@ export const previewRouterFactory = ({ } const { commits } = commitsObj if (!commits || commits.length === 0) { - return res.sendFile(noPreviewImage) + return res.sendFile(await noPreviewImage()) } const lastCommit = commits[0] @@ -230,7 +234,7 @@ export const previewRouterFactory = ({ const { hasPermissions, httpErrorCode } = await checkStreamPermissions(req) if (!hasPermissions) { // return res.status( httpErrorCode ).end() - return res.sendFile(httpErrorImage(httpErrorCode)) + return res.sendFile(await httpErrorImage(httpErrorCode)) } const projectDb = await getProjectDbClient({ projectId: req.params.streamId }) @@ -239,7 +243,7 @@ export const previewRouterFactory = ({ const commit = await getCommit(req.params.commitId, { streamId: req.params.streamId }) - if (!commit) return res.sendFile(noPreviewImage) + if (!commit) return res.sendFile(await noPreviewImage()) const getObjectPreviewBufferOrFilepath = getObjectPreviewBufferOrFilepathFactory({ logger: req.log, diff --git a/packages/server/modules/previews/resultListener.ts b/packages/server/modules/previews/resultListener.ts index 1bc72c943..d3f750ecb 100644 --- a/packages/server/modules/previews/resultListener.ts +++ b/packages/server/modules/previews/resultListener.ts @@ -85,7 +85,6 @@ export const consumePreviewResultFactory = 'base64' ) - // @ts-expect-error this is a mismatch with node 18 and 22 types. upgrading to new node will fix it const id = crypto.createHash('md5').update(data).digest('hex') if (i++ === 0) { @@ -104,7 +103,6 @@ export const consumePreviewResultFactory = }) const png = fullImg.png({ quality: 95 }) const buff = await png.toBuffer() - // @ts-expect-error this is a mismatch with node 18 and 22 types. upgrading to new node will fix it const fullImgId = crypto.createHash('md5').update(buff).digest('hex') await storePreview({ preview: { id: fullImgId, data: buff } }) diff --git a/packages/server/modules/previews/services/management.ts b/packages/server/modules/previews/services/management.ts index 2fa94a958..f3a8768b9 100644 --- a/packages/server/modules/previews/services/management.ts +++ b/packages/server/modules/previews/services/management.ts @@ -15,9 +15,8 @@ import { Roles, Scopes } from '@speckle/shared' import type { Logger } from 'pino' import { PreviewPriority, PreviewStatus } from '@/modules/previews/domain/consts' import { ProjectRecordVisibility } from '@/modules/core/helpers/types' +import { fileURLToPath } from 'url' -const noPreviewImage = require.resolve('#/assets/previews/images/no_preview.png') -const previewErrorImage = require.resolve('#/assets/previews/images/preview_error.png') const defaultAngle = '0' export const getObjectPreviewBufferOrFilepathFactory = @@ -29,6 +28,11 @@ export const getObjectPreviewBufferOrFilepathFactory = logger: Logger }): GetObjectPreviewBufferOrFilepath => async ({ streamId, objectId, angle }) => { + const [noPreviewImage, previewErrorImage] = await Promise.all([ + fileURLToPath(import.meta.resolve('#/assets/previews/images/no_preview.png')), + fileURLToPath(import.meta.resolve('#/assets/previews/images/preview_error.png')) + ]) + angle = angle || defaultAngle const boundLogger = deps.logger.child({ streamId, objectId, angle }) @@ -44,7 +48,9 @@ export const getObjectPreviewBufferOrFilepathFactory = if (!dbObj) { return { type: 'file', - file: require.resolve('#/assets/previews/images/preview_404.png'), + file: fileURLToPath( + import.meta.resolve('#/assets/previews/images/preview_404.png') + ), error: true, errorCode: 'OBJECT_NOT_FOUND' } diff --git a/packages/server/modules/pwdreset/index.ts b/packages/server/modules/pwdreset/index.ts index d159cdeff..87aa276d9 100644 --- a/packages/server/modules/pwdreset/index.ts +++ b/packages/server/modules/pwdreset/index.ts @@ -1,7 +1,7 @@ import { moduleLogger } from '@/observability/logging' import RestSetup from '@/modules/pwdreset/rest' import { SpeckleModule } from '@/modules/shared/helpers/typeHelper' -import { noop } from 'lodash' +import { noop } from 'lodash-es' export const init: SpeckleModule['init'] = ({ app }) => { moduleLogger.info('♻️ Init pwd reset module') diff --git a/packages/server/modules/pwdreset/migrations/20210304111614_pwdreset.js b/packages/server/modules/pwdreset/migrations/20210304111614_pwdreset.js index 3f779248b..e9af4bd07 100644 --- a/packages/server/modules/pwdreset/migrations/20210304111614_pwdreset.js +++ b/packages/server/modules/pwdreset/migrations/20210304111614_pwdreset.js @@ -1,5 +1,5 @@ /* istanbul ignore file */ -exports.up = async (knex) => { +const up = async (knex) => { await knex.schema.createTable('pwdreset_tokens', (table) => { table.string('id').defaultTo(knex.raw('gen_random_uuid()')).primary() table.string('email', 256).notNullable() @@ -7,6 +7,8 @@ exports.up = async (knex) => { }) } -exports.down = async (knex) => { +const down = async (knex) => { await knex.schema.dropTableIfExists('pwdreset_tokens') } + +export { up, down } diff --git a/packages/server/modules/pwdreset/repositories/index.ts b/packages/server/modules/pwdreset/repositories/index.ts index 000697b3f..cc3a168b7 100644 --- a/packages/server/modules/pwdreset/repositories/index.ts +++ b/packages/server/modules/pwdreset/repositories/index.ts @@ -1,6 +1,5 @@ import crs from 'crypto-random-string' import { PasswordResetTokens } from '@/modules/core/dbSchema' -import { StringChain } from 'lodash' import dayjs from 'dayjs' import { InvalidArgumentError } from '@/modules/shared/errors' import { Knex } from 'knex' @@ -14,7 +13,7 @@ import { export type PasswordResetTokenRecord = { id: string email: string - createdAt: StringChain + createdAt: string } const tables = { diff --git a/packages/server/modules/schema.ts b/packages/server/modules/schema.ts deleted file mode 100644 index f26149cc6..000000000 --- a/packages/server/modules/schema.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { graphSchema } from '@/modules/index' - -/** - * Used in codegen.yml - */ - -const schema = graphSchema() -export default schema diff --git a/packages/server/modules/serverinvites/graph/resolvers/serverInvites.ts b/packages/server/modules/serverinvites/graph/resolvers/serverInvites.ts index 744c5eb57..e64b85065 100644 --- a/packages/server/modules/serverinvites/graph/resolvers/serverInvites.ts +++ b/packages/server/modules/serverinvites/graph/resolvers/serverInvites.ts @@ -17,7 +17,7 @@ import { getServerInviteForTokenFactory } from '@/modules/serverinvites/services/retrieval' import { authorizeResolver } from '@/modules/shared' -import { chunk } from 'lodash' +import { chunk } from 'lodash-es' import { Resolvers, TokenResourceIdentifierType @@ -158,7 +158,7 @@ const buildCreateAndSendServerOrProjectInvite = () => finalizeInvite: buildFinalizeProjectInvite() }) -export = { +export default { Query: { async streamInvite(_parent, args, context) { const { streamId, token } = args diff --git a/packages/server/modules/serverinvites/migrations/20210303185834_invites.js b/packages/server/modules/serverinvites/migrations/20210303185834_invites.js index 46ff033f5..bc21e0569 100644 --- a/packages/server/modules/serverinvites/migrations/20210303185834_invites.js +++ b/packages/server/modules/serverinvites/migrations/20210303185834_invites.js @@ -1,5 +1,5 @@ // /* istanbul ignore file */ -exports.up = async (knex) => { +const up = async (knex) => { await knex.schema.createTable('server_invites', (table) => { table.string('id').defaultTo(knex.raw('gen_random_uuid()')).primary() table.string('email', 256).unique().notNullable() @@ -13,6 +13,8 @@ exports.up = async (knex) => { }) } -exports.down = async (knex) => { +const down = async (knex) => { await knex.schema.dropTableIfExists('server_invites') } + +export { up, down } diff --git a/packages/server/modules/serverinvites/migrations/20220629110918_server_invites_rework.js b/packages/server/modules/serverinvites/migrations/20220629110918_server_invites_rework.js index c3720b69d..95347f728 100644 --- a/packages/server/modules/serverinvites/migrations/20220629110918_server_invites_rework.js +++ b/packages/server/modules/serverinvites/migrations/20220629110918_server_invites_rework.js @@ -7,7 +7,7 @@ const NEW_IDX_2 = ['resourceTarget', 'resourceId'] * @param { import("knex").Knex } knex * @returns { Promise } */ -exports.up = async function (knex) { +const up = async function (knex) { // First lets delete all invites with an invalid user ID, otherwise // the new foreign key will fail await knex(TABLE_NAME) @@ -39,7 +39,7 @@ exports.up = async function (knex) { * @param { import("knex").Knex } knex * @returns { Promise } */ -exports.down = async function (knex) { +const down = async function (knex) { // Since we want to add back the unique idx on email, we need to delete rows that have duplicate emails await knex(TABLE_NAME) .whereIn( @@ -56,3 +56,5 @@ exports.down = async function (knex) { table.unique('email') }) } + +export { up, down } diff --git a/packages/server/modules/serverinvites/migrations/20220722092821_add_invite_token_field.js b/packages/server/modules/serverinvites/migrations/20220722092821_add_invite_token_field.js index e420b666e..725a4c0dc 100644 --- a/packages/server/modules/serverinvites/migrations/20220722092821_add_invite_token_field.js +++ b/packages/server/modules/serverinvites/migrations/20220722092821_add_invite_token_field.js @@ -1,10 +1,10 @@ -const { ServerInvites } = require('@/modules/core/dbSchema') +import { ServerInvites } from '@/modules/core/dbSchema' /** * @param { import("knex").Knex } knex * @returns { Promise } */ -exports.up = async function (knex) { +async function up(knex) { await knex.schema.alterTable(ServerInvites.name, (table) => { // Add token field table.string('token', 256).defaultTo('').notNullable() @@ -23,9 +23,11 @@ exports.up = async function (knex) { * @param { import("knex").Knex } knex * @returns { Promise } */ -exports.down = async function (knex) { +async function down(knex) { await knex.schema.alterTable(ServerInvites.name, (table) => { // Drop token field table.dropColumn('token') }) } + +export { up, down } diff --git a/packages/server/modules/serverinvites/migrations/20230517122919_clean_up_invalid_stream_invites.ts b/packages/server/modules/serverinvites/migrations/20230517122919_clean_up_invalid_stream_invites.ts index 1bfd22a3f..4ef49be0f 100644 --- a/packages/server/modules/serverinvites/migrations/20230517122919_clean_up_invalid_stream_invites.ts +++ b/packages/server/modules/serverinvites/migrations/20230517122919_clean_up_invalid_stream_invites.ts @@ -1,5 +1,5 @@ import { Knex } from 'knex' -import { chunk } from 'lodash' +import { chunk } from 'lodash-es' const INVITES_TABLE = 'server_invites' diff --git a/packages/server/modules/serverinvites/repositories/serverInvites.ts b/packages/server/modules/serverinvites/repositories/serverInvites.ts index b521cd8d2..2861c3e5e 100644 --- a/packages/server/modules/serverinvites/repositories/serverInvites.ts +++ b/packages/server/modules/serverinvites/repositories/serverInvites.ts @@ -5,7 +5,7 @@ import { UserWithOptionalRole } from '@/modules/core/repositories/users' import { resolveTarget, buildUserTarget } from '@/modules/serverinvites/helpers/core' -import { isObjectLike, uniq } from 'lodash' +import { isObjectLike, uniq } from 'lodash-es' import { ExtendedInvite, InviteResourceTarget, diff --git a/packages/server/modules/serverinvites/services/coreResourceCollection.ts b/packages/server/modules/serverinvites/services/coreResourceCollection.ts index f23c4ef04..9fd9cb882 100644 --- a/packages/server/modules/serverinvites/services/coreResourceCollection.ts +++ b/packages/server/modules/serverinvites/services/coreResourceCollection.ts @@ -11,7 +11,7 @@ import { } from '@/modules/serverinvites/helpers/core' import { authorizeResolver } from '@/modules/shared' import { Roles } from '@speckle/shared' -import { flatten } from 'lodash' +import { flatten } from 'lodash-es' import { GetStream } from '@/modules/core/domain/streams/operations' const collectAndValidateServerTargetFactory = diff --git a/packages/server/modules/serverinvites/services/processing.ts b/packages/server/modules/serverinvites/services/processing.ts index a22462fba..9e454a67b 100644 --- a/packages/server/modules/serverinvites/services/processing.ts +++ b/packages/server/modules/serverinvites/services/processing.ts @@ -32,7 +32,7 @@ import { ValidateServerInvite } from '@/modules/serverinvites/services/operations' import { ensureError, MaybeNullOrUndefined } from '@speckle/shared' -import { noop } from 'lodash' +import { noop } from 'lodash-es' import { ServerInvitesEvents } from '@/modules/serverinvites/domain/events' import { TokenResourceIdentifier } from '@/modules/core/domain/tokens/types' import { EventBusEmit } from '@/modules/shared/services/eventBus' diff --git a/packages/server/modules/serverinvites/services/projectInviteManagement.ts b/packages/server/modules/serverinvites/services/projectInviteManagement.ts index f9d479e97..eb8df8efb 100644 --- a/packages/server/modules/serverinvites/services/projectInviteManagement.ts +++ b/packages/server/modules/serverinvites/services/projectInviteManagement.ts @@ -52,7 +52,7 @@ import { ServerRoles, StreamRoles } from '@speckle/shared' -import { has } from 'lodash' +import { has } from 'lodash-es' type FullProjectInviteCreateInput = ProjectInviteCreateInput & { projectId: string } diff --git a/packages/server/modules/serverinvites/services/retrieval.ts b/packages/server/modules/serverinvites/services/retrieval.ts index 0a0cfaa15..b5b48b83b 100644 --- a/packages/server/modules/serverinvites/services/retrieval.ts +++ b/packages/server/modules/serverinvites/services/retrieval.ts @@ -1,7 +1,7 @@ import { ServerInviteGraphQLReturnType } from '@/modules/core/helpers/graphTypes' import { resolveTarget } from '@/modules/serverinvites/helpers/core' import { Nullable } from '@speckle/shared' -import { keyBy, uniq } from 'lodash' +import { keyBy, uniq } from 'lodash-es' import { FindServerInvite } from '@/modules/serverinvites/domain/operations' import { GetInvitationTargetUsers } from '@/modules/serverinvites/services/operations' import { GetUsers } from '@/modules/core/domain/users/operations' diff --git a/packages/server/modules/serverinvites/tests/invites.spec.ts b/packages/server/modules/serverinvites/tests/invites.spec.ts index 43925fa8c..3c6bff0e6 100644 --- a/packages/server/modules/serverinvites/tests/invites.spec.ts +++ b/packages/server/modules/serverinvites/tests/invites.spec.ts @@ -13,7 +13,6 @@ import { createStreamInviteDirectly, validateInviteExistanceFromEmail } from '@/test/speckle-helpers/inviteHelper' -import { EmailSendingServiceMock } from '@/test/mocks/global' import db from '@/db/knex' import { findInviteFactory } from '@/modules/serverinvites/repositories/serverInvites' import { BasicTestUser, createTestUser } from '@/test/authHelper' @@ -41,9 +40,10 @@ import { UseStreamInviteDocument } from '@/test/graphql/generated/graphql' import { ServerInviteRecord } from '@/modules/serverinvites/domain/types' -import { reduce } from 'lodash' +import { reduce } from 'lodash-es' import { grantStreamPermissionsFactory } from '@/modules/core/repositories/streams' import { getFeatureFlags } from '@/modules/shared/helpers/envHelper' +import { createEmailListener, TestEmailListener } from '@/test/speckle-helpers/email' const { FF_PERSONAL_PROJECTS_LIMITS_ENABLED } = getFeatureFlags() @@ -55,8 +55,6 @@ const findInvite = findInviteFactory({ db }) const createInviteDirectly = createStreamInviteDirectly const grantStreamPermissions = grantStreamPermissionsFactory({ db }) -const mailerMock = EmailSendingServiceMock - describe('[Stream & Server Invites]', () => { const me: BasicTestUser = { name: 'Authenticated server invites guy', @@ -86,6 +84,8 @@ describe('[Stream & Server Invites]', () => { ownerId: '' } + let emailListener: TestEmailListener + before(async () => { await cleanup() @@ -96,14 +96,16 @@ describe('[Stream & Server Invites]', () => { [myPrivateStream, me], [otherGuysStream, otherGuy] ]) + emailListener = await createEmailListener() }) after(async () => { await cleanup() + await emailListener.destroy() }) afterEach(() => { - mailerMock.resetMockedFunctions() + emailListener.reset() }) describe('When user authenticated', () => { @@ -138,10 +140,7 @@ describe('[Stream & Server Invites]', () => { const messagePart2 = 'yepppppp' const unsanitaryMessage = `${messagePart1} ` - const sendEmailInvocations = mailerMock.hijackFunction( - 'sendEmail', - async () => true - ) + const { getSends } = emailListener.listen({ times: 2 }) const result = await createInvite({ email: targetEmail, @@ -153,8 +152,8 @@ describe('[Stream & Server Invites]', () => { expect(result.data?.serverInviteCreate).to.be.ok // Check that email was sent out - expect(sendEmailInvocations.args).to.have.lengthOf(1) - const emailParams = sendEmailInvocations.args[0][0] + expect(getSends()).to.have.lengthOf(1) + const emailParams = getSends()[0] expect(emailParams).to.be.ok expect(emailParams.to).to.eq(targetEmail) expect(emailParams.subject).to.be.ok @@ -295,10 +294,7 @@ describe('[Stream & Server Invites]', () => { const unsanitaryMessage = `${messagePart1} ` const targetEmail = email || user?.email - const sendEmailInvocations = mailerMock.hijackFunction( - 'sendEmail', - async () => true - ) + const { getSends } = emailListener.listen({ times: 2 }) if (projectInvite) { const result = await createProjectInvite({ @@ -328,7 +324,10 @@ describe('[Stream & Server Invites]', () => { } // Check that email was sent out - const emailParams = sendEmailInvocations.args[0][0] + const emailSends = getSends() + expect(emailSends).to.have.lengthOf(1) + + const emailParams = emailSends[0] expect(emailParams).to.be.ok expect(emailParams.to).to.eq(targetEmail) expect(emailParams.subject).to.be.ok @@ -504,11 +503,7 @@ describe('[Stream & Server Invites]', () => { }) it('they can resend pre-existing invites irregardless of type', async () => { - const sendEmailInvocations = mailerMock.hijackFunction( - 'sendEmail', - async () => true, - { times: invites.length } - ) + const { getSends } = emailListener.listen({ times: invites.length }) const inviteIds = invites.map((i) => i.inviteId) const inviteLastRemindedDates = reduce( @@ -534,7 +529,7 @@ describe('[Stream & Server Invites]', () => { expect(result.errors).to.not.be.ok } - expect(sendEmailInvocations.length()).to.eq(inviteIds.length) + expect(getSends().length).to.eq(inviteIds.length) const newInviteLastRemindedDates = reduce( await ServerInvites.knex().whereIn( @@ -598,11 +593,7 @@ describe('[Stream & Server Invites]', () => { const emails = ['abababa1@mail.com', 'abababa2@mail.com', 'abababa3@mail.com'] const message = 'ayyoyoyoyoy' - const sendEmailInvocations = mailerMock.hijackFunction( - 'sendEmail', - async () => true, - { times: emails.length } - ) + const { getSends } = emailListener.listen({ times: emails.length }) const result = await apollo.execute(BatchCreateServerInviteDocument, { input: emails.map((email) => ({ @@ -614,11 +605,10 @@ describe('[Stream & Server Invites]', () => { expect(result.errors).to.not.be.ok expect(result.data?.serverInviteBatchCreate).to.be.ok - expect(sendEmailInvocations.length()).to.eq(emails.length) + const emailSends = getSends() + expect(emailSends.length).to.eq(emails.length) for (const email of emails) { - const emailParams = sendEmailInvocations.args.find( - ([p]) => p.to === email - )?.[0] + const emailParams = emailSends.find((p) => p.to === email) expect(emailParams).to.be.ok expect(emailParams!.html).to.contain(message) @@ -652,11 +642,7 @@ describe('[Stream & Server Invites]', () => { } ] - const sendEmailInvocations = mailerMock.hijackFunction( - 'sendEmail', - async () => false, - { times: inputs.length } - ) + const { getSends } = emailListener.listen({ times: inputs.length }) const result = await apollo.execute(BatchCreateStreamInviteDocument, { input: inputs @@ -665,11 +651,12 @@ describe('[Stream & Server Invites]', () => { expect(result.data?.streamInviteBatchCreate).to.be.ok expect(result.errors).to.not.be.ok - expect(sendEmailInvocations.length()).to.eq(inputs.length) + const emailSends = getSends() + expect(emailSends.length).to.eq(inputs.length) for (const inputData of inputs) { - const emailParams = sendEmailInvocations.args.find(([p]) => + const emailParams = emailSends.find((p) => inputData.email ? p.to === inputData.email : p.to === otherGuy.email - )?.[0] + ) expect(emailParams).to.be.ok expect(emailParams!.html).to.contain(inputData.message) expect(emailParams!.text).to.contain(inputData.message) diff --git a/packages/server/modules/shared/authz.ts b/packages/server/modules/shared/authz.ts index 1d73d151f..4cbc2f397 100644 --- a/packages/server/modules/shared/authz.ts +++ b/packages/server/modules/shared/authz.ts @@ -19,7 +19,7 @@ import { import { isResourceAllowed } from '@/modules/core/helpers/token' import { UserRoleData } from '@/modules/shared/domain/rolesAndScopes/types' import db from '@/db/knex' -import { +import type { AuthContext, AuthParams, AuthResult, diff --git a/packages/server/modules/shared/command.ts b/packages/server/modules/shared/command.ts index e492b8c64..1ca5922e9 100644 --- a/packages/server/modules/shared/command.ts +++ b/packages/server/modules/shared/command.ts @@ -9,7 +9,7 @@ import { import { withOperationLogging } from '@/observability/domain/businessLogging' import { MaybeAsync } from '@speckle/shared' import { Knex } from 'knex' -import { isBoolean } from 'lodash' +import { isBoolean } from 'lodash-es' import { Logger } from 'pino' /** diff --git a/packages/server/modules/shared/domain/rolesAndScopes/logic.ts b/packages/server/modules/shared/domain/rolesAndScopes/logic.ts index f578a1d33..bddce47a5 100644 --- a/packages/server/modules/shared/domain/rolesAndScopes/logic.ts +++ b/packages/server/modules/shared/domain/rolesAndScopes/logic.ts @@ -1,6 +1,6 @@ import { UserRoleData } from '@/modules/shared/domain/rolesAndScopes/types' import { AvailableRoles } from '@speckle/shared' -import { isUndefined } from 'lodash' +import { isUndefined } from 'lodash-es' /** * Order roles by weight in descending order (meaning - highest permission roles come first) diff --git a/packages/server/modules/shared/errors/base.ts b/packages/server/modules/shared/errors/base.ts index d604a0d0e..ca6fd9556 100644 --- a/packages/server/modules/shared/errors/base.ts +++ b/packages/server/modules/shared/errors/base.ts @@ -1,5 +1,6 @@ import { Merge } from 'type-fest' -import { VError, Options, Info } from 'verror' +import type { Options, Info } from 'verror' +import VError from 'verror' export type ExtendedOptions = Merge< Options, diff --git a/packages/server/modules/shared/errors/index.ts b/packages/server/modules/shared/errors/index.ts index 24e25a002..ff8fac6cd 100644 --- a/packages/server/modules/shared/errors/index.ts +++ b/packages/server/modules/shared/errors/index.ts @@ -157,5 +157,11 @@ export class LoaderUnsupportedError extends BaseError { static statusCode = 500 } +export class TestOnlyLogicError extends BaseError { + static code = 'TEST_ONLY_LOGIC_ERROR' + static defaultMessage = 'This code should only be executed during tests' + static statusCode = 500 +} + export { BaseError } export type { Info } diff --git a/packages/server/modules/shared/helpers/dbHelper.ts b/packages/server/modules/shared/helpers/dbHelper.ts index e472797fd..51567e44e 100644 --- a/packages/server/modules/shared/helpers/dbHelper.ts +++ b/packages/server/modules/shared/helpers/dbHelper.ts @@ -7,7 +7,7 @@ import { base64Decode, base64Encode } from '@/modules/shared/helpers/cryptoHelpe import dayjs, { Dayjs } from 'dayjs' import { MaybeNullOrUndefined, Nullable } from '@speckle/shared' import { SchemaConfig } from '@/modules/core/dbSchema' -import { has, isObjectLike, isString, mapValues, pick, times } from 'lodash' +import { has, isObjectLike, isString, mapValues, pick, times } from 'lodash-es' export type Collection = { cursor: string | null diff --git a/packages/server/modules/shared/helpers/envHelper.ts b/packages/server/modules/shared/helpers/envHelper.ts index 9a69d8201..783a8bf00 100644 --- a/packages/server/modules/shared/helpers/envHelper.ts +++ b/packages/server/modules/shared/helpers/envHelper.ts @@ -1,5 +1,5 @@ import { MisconfiguredEnvironmentError } from '@/modules/shared/errors' -import { has, trimEnd } from 'lodash' +import { has, trimEnd } from 'lodash-es' import * as Environment from '@speckle/shared/environment' import { ensureError, Nullable } from '@speckle/shared' diff --git a/packages/server/modules/shared/helpers/errorHelper.ts b/packages/server/modules/shared/helpers/errorHelper.ts index 93f006046..51cb60b87 100644 --- a/packages/server/modules/shared/helpers/errorHelper.ts +++ b/packages/server/modules/shared/helpers/errorHelper.ts @@ -8,7 +8,7 @@ import { } from '@/modules/shared/errors' import { SsoSessionMissingOrExpiredError } from '@/modules/workspacesCore/errors' import { Authz, ensureError, throwUncoveredError } from '@speckle/shared' -import { VError } from 'verror' +import VError from 'verror' /** * Resolve cause correctly depending on whether its a VError or basic Error diff --git a/packages/server/modules/shared/helpers/graphqlHelper.ts b/packages/server/modules/shared/helpers/graphqlHelper.ts index ee0d224de..31fb4e565 100644 --- a/packages/server/modules/shared/helpers/graphqlHelper.ts +++ b/packages/server/modules/shared/helpers/graphqlHelper.ts @@ -63,3 +63,7 @@ export const isUserGraphqlError = (error: GraphQLError): boolean => { const code = error.extensions?.code as string return userCodes.includes(code) } + +export const isGraphQLError = (error: unknown): error is GraphQLError => { + return error instanceof GraphQLError +} diff --git a/packages/server/modules/shared/helpers/mocks.ts b/packages/server/modules/shared/helpers/mocks.ts index e022f0524..ebd77aa69 100644 --- a/packages/server/modules/shared/helpers/mocks.ts +++ b/packages/server/modules/shared/helpers/mocks.ts @@ -4,7 +4,7 @@ import { db } from '@/db/knex' import { ResolverFn, Resolvers } from '@/modules/core/graph/generated/graphql' import { IMockStore, IMocks, isRef, Ref } from '@graphql-tools/mock' import { GraphQLResolveInfo } from 'graphql' -import { get, has, isArray, isObjectLike, random } from 'lodash' +import { get, has, isArray, isObjectLike, random } from 'lodash-es' export type SpeckleModuleMocksConfig = { resolvers?: (params: { diff --git a/packages/server/modules/shared/services/eventBus.ts b/packages/server/modules/shared/services/eventBus.ts index aff9f2587..a034c939b 100644 --- a/packages/server/modules/shared/services/eventBus.ts +++ b/packages/server/modules/shared/services/eventBus.ts @@ -53,6 +53,14 @@ import { fileuploadEventNamespace, FileuploadEventsPayloads } from '@/modules/fileuploads/domain/events' +import { + emailsEventNamespace, + EmailsEventsPayloads +} from '@/modules/emails/domain/events' +import { + notificationsEventNamespace, + NotificationsEventsPayloads +} from '@/modules/notifications/domain/events' type AllEventsWildcard = '**' type EventWildcard = '*' @@ -83,6 +91,8 @@ type EventsByNamespace = { [automationRunEventsNamespace]: AutomationRunEventsPayloads [multiregionEventNamespace]: MultiregionEventsPayloads [fileuploadEventNamespace]: FileuploadEventsPayloads + [emailsEventNamespace]: EmailsEventsPayloads + [notificationsEventNamespace]: NotificationsEventsPayloads } type EventTypes = UnionToIntersection diff --git a/packages/server/modules/shared/utils/caching.ts b/packages/server/modules/shared/utils/caching.ts index 596c72c52..2e19e7124 100644 --- a/packages/server/modules/shared/utils/caching.ts +++ b/packages/server/modules/shared/utils/caching.ts @@ -6,7 +6,7 @@ import { cacheLogger } from '@/observability/logging' import TTLCache from '@isaacs/ttlcache' import { MaybeAsync, TIME_MS } from '@speckle/shared' import Redis from 'ioredis' -import { isNumber } from 'lodash' +import { isNumber } from 'lodash-es' export interface CacheProvider { get: (key: string) => Promise diff --git a/packages/server/modules/stats/graph/resolvers/stats.ts b/packages/server/modules/stats/graph/resolvers/stats.ts index 30dba86f9..2433173e4 100644 --- a/packages/server/modules/stats/graph/resolvers/stats.ts +++ b/packages/server/modules/stats/graph/resolvers/stats.ts @@ -19,7 +19,7 @@ const dummyHistory = [ { '11': 0 } ] -export = { +export default { Query: { /** * @deprecated('Use admin.serverStatistics') diff --git a/packages/server/modules/webhooks/graph/resolvers/webhooks.ts b/packages/server/modules/webhooks/graph/resolvers/webhooks.ts index 41174874d..04ec85e48 100644 --- a/packages/server/modules/webhooks/graph/resolvers/webhooks.ts +++ b/packages/server/modules/webhooks/graph/resolvers/webhooks.ts @@ -61,7 +61,7 @@ const streamWebhooksResolver = async ( return { items, totalCount: items.length } } -export = { +export default { Webhook: { projectId: (parent) => parent.streamId, hasSecret: (parent) => !!parent.secret?.length, diff --git a/packages/server/modules/webhooks/migrations/20210701180000-webhooks.js b/packages/server/modules/webhooks/migrations/20210701180000-webhooks.js index 98c51a8db..baa5d96c8 100644 --- a/packages/server/modules/webhooks/migrations/20210701180000-webhooks.js +++ b/packages/server/modules/webhooks/migrations/20210701180000-webhooks.js @@ -1,7 +1,7 @@ /* istanbul ignore file */ 'use strict' -exports.up = async (knex) => { +const up = async (knex) => { await knex.schema.createTable('webhooks_config', (table) => { table.string('id').primary() table.string('streamId', 10).references('id').inTable('streams').onDelete('cascade') @@ -34,7 +34,9 @@ exports.up = async (knex) => { }) } -exports.down = async (knex) => { +const down = async (knex) => { await knex.schema.dropTableIfExists('webhooks_events') await knex.schema.dropTableIfExists('webhooks_config') } + +export { up, down } diff --git a/packages/server/modules/webhooks/tests/webhooks.spec.ts b/packages/server/modules/webhooks/tests/webhooks.spec.ts index d1b4072fb..d8ed41607 100644 --- a/packages/server/modules/webhooks/tests/webhooks.spec.ts +++ b/packages/server/modules/webhooks/tests/webhooks.spec.ts @@ -85,7 +85,7 @@ import { validateStreamAccessFactory } from '@/modules/core/services/streams/access' import { authorizeResolver } from '@/modules/shared' -import { omit } from 'lodash' +import { omit } from 'lodash-es' const getServerInfo = getServerInfoFactory({ db }) const getUser = getUserFactory({ db }) diff --git a/packages/server/modules/workspaces/events/eventListener.ts b/packages/server/modules/workspaces/events/eventListener.ts index 4859def72..babdef9c3 100644 --- a/packages/server/modules/workspaces/events/eventListener.ts +++ b/packages/server/modules/workspaces/events/eventListener.ts @@ -139,7 +139,7 @@ import { getProjectWorkspaceFactory } from '@/modules/workspaces/repositories/pr import { getWorkspaceModelCountFactory } from '@/modules/workspaces/services/workspaceLimits' import { getPaginatedProjectModelsTotalCountFactory } from '@/modules/core/repositories/branches' import { buildWorkspaceTrackingPropertiesFactory } from '@/modules/workspaces/services/tracking' -import { assign } from 'lodash' +import { assign } from 'lodash-es' import { WorkspacePlanStatuses } from '@/modules/cross-server-sync/graph/generated/graphql' import { GatekeeperEvents } from '@/modules/gatekeeperCore/domain/events' import { GetUser } from '@/modules/core/domain/users/operations' diff --git a/packages/server/modules/workspaces/graph/dataloaders/workspaces.ts b/packages/server/modules/workspaces/graph/dataloaders/workspaces.ts index ca5d535e7..dc9bc3652 100644 --- a/packages/server/modules/workspaces/graph/dataloaders/workspaces.ts +++ b/packages/server/modules/workspaces/graph/dataloaders/workspaces.ts @@ -11,7 +11,7 @@ import { WorkspaceDomain, WorkspaceWithOptionalRole } from '@/modules/workspacesCore/domain/types' -import { keyBy } from 'lodash' +import { keyBy } from 'lodash-es' const { FF_WORKSPACES_MODULE_ENABLED } = getFeatureFlags() diff --git a/packages/server/modules/workspaces/graph/mocks/workspaces.ts b/packages/server/modules/workspaces/graph/mocks/workspaces.ts index 7f6978eb6..3607622be 100644 --- a/packages/server/modules/workspaces/graph/mocks/workspaces.ts +++ b/packages/server/modules/workspaces/graph/mocks/workspaces.ts @@ -3,7 +3,7 @@ import { listMock, SpeckleModuleMocksConfig } from '@/modules/shared/helpers/moc import { getFeatureFlags } from '@/modules/shared/helpers/envHelper' import { faker } from '@faker-js/faker' import { Roles } from '@speckle/shared' -import { omit, times } from 'lodash' +import { omit, times } from 'lodash-es' import { WorkspaceNotFoundError } from '@/modules/workspaces/errors/workspace' const { FF_WORKSPACES_MODULE_ENABLED } = getFeatureFlags() diff --git a/packages/server/modules/workspaces/graph/resolvers/workspaces.ts b/packages/server/modules/workspaces/graph/resolvers/workspaces.ts index b5434d1a2..c69050792 100644 --- a/packages/server/modules/workspaces/graph/resolvers/workspaces.ts +++ b/packages/server/modules/workspaces/graph/resolvers/workspaces.ts @@ -121,7 +121,7 @@ import { removeNullOrUndefinedKeys, throwUncoveredError } from '@speckle/shared' -import { chunk, omit } from 'lodash' +import { chunk, omit } from 'lodash-es' import { findEmailsByUserIdFactory, findVerifiedEmailsByUserIdFactory, @@ -425,7 +425,7 @@ const throwIfRateLimited = throwIfRateLimitedFactory({ rateLimiterEnabled: isRateLimiterEnabled() }) -export = FF_WORKSPACES_MODULE_ENABLED +export default FF_WORKSPACES_MODULE_ENABLED ? ({ Query: { workspace: async (_parent, args, ctx) => { diff --git a/packages/server/modules/workspaces/helpers/sso.ts b/packages/server/modules/workspaces/helpers/sso.ts index ac21b89b0..7e2c99bb2 100644 --- a/packages/server/modules/workspaces/helpers/sso.ts +++ b/packages/server/modules/workspaces/helpers/sso.ts @@ -10,7 +10,7 @@ import { } from '@/modules/workspaces/errors/sso' import { OidcProvider } from '@/modules/workspaces/domain/sso/types' import { Request } from 'express' -import { omit } from 'lodash' +import { omit } from 'lodash-es' declare module 'express-session' { interface SessionData { diff --git a/packages/server/modules/workspaces/index.ts b/packages/server/modules/workspaces/index.ts index bf3ccab9c..691d98d28 100644 --- a/packages/server/modules/workspaces/index.ts +++ b/packages/server/modules/workspaces/index.ts @@ -121,4 +121,4 @@ const workspacesModule: SpeckleModule = { } } -export = workspacesModule +export default workspacesModule diff --git a/packages/server/modules/workspaces/repositories/sso.ts b/packages/server/modules/workspaces/repositories/sso.ts index 431be4dee..fa7ba052d 100644 --- a/packages/server/modules/workspaces/repositories/sso.ts +++ b/packages/server/modules/workspaces/repositories/sso.ts @@ -21,7 +21,7 @@ import { SsoProviderTypeNotSupportedError } from '@/modules/workspaces/errors/ss import { Workspace, WorkspaceAcl } from '@/modules/workspacesCore/domain/types' import Redis from 'ioredis' import { Knex } from 'knex' -import { omit } from 'lodash' +import { omit } from 'lodash-es' type Crypt = (input: string) => Promise diff --git a/packages/server/modules/workspaces/repositories/workspaces.ts b/packages/server/modules/workspaces/repositories/workspaces.ts index 1955b7726..ec81d0597 100644 --- a/packages/server/modules/workspaces/repositories/workspaces.ts +++ b/packages/server/modules/workspaces/repositories/workspaces.ts @@ -73,7 +73,7 @@ import { } from '@/modules/core/dbSchema' import { removePrivateFields, UserRecord } from '@/modules/core/helpers/userHelper' -import { clamp, has, isObjectLike } from 'lodash' +import { clamp, has, isObjectLike } from 'lodash-es' import { WorkspaceCreationState, WorkspaceTeamMember diff --git a/packages/server/modules/workspaces/roles.ts b/packages/server/modules/workspaces/roles.ts index df43e865a..64f8f35e3 100644 --- a/packages/server/modules/workspaces/roles.ts +++ b/packages/server/modules/workspaces/roles.ts @@ -1,6 +1,6 @@ import { UserWorkspaceRole } from '@/modules/shared/domain/rolesAndScopes/types' import { Roles, RoleInfo } from '@speckle/shared' -import { pick } from 'lodash' +import { pick } from 'lodash-es' const aclTableName = 'workspace_acl' const resourceTarget = 'workspaces' diff --git a/packages/server/modules/workspaces/services/management.ts b/packages/server/modules/workspaces/services/management.ts index 51578d365..f658781f6 100644 --- a/packages/server/modules/workspaces/services/management.ts +++ b/packages/server/modules/workspaces/services/management.ts @@ -60,7 +60,7 @@ import { import { DeleteAllResourceInvites } from '@/modules/serverinvites/domain/operations' import { WorkspaceInviteResourceType } from '@/modules/workspacesCore/domain/constants' import { ProjectInviteResourceType } from '@/modules/serverinvites/domain/constants' -import { chunk, isEmpty, omit } from 'lodash' +import { chunk, isEmpty, omit } from 'lodash-es' import { userEmailsCompliantWithWorkspaceDomains } from '@/modules/workspaces/domain/logic' import { workspaceRoles as workspaceRoleDefinitions } from '@/modules/workspaces/roles' import { blockedDomains } from '@speckle/shared' diff --git a/packages/server/modules/workspaces/services/projects.ts b/packages/server/modules/workspaces/services/projects.ts index e032197c0..c2e7ec607 100644 --- a/packages/server/modules/workspaces/services/projects.ts +++ b/packages/server/modules/workspaces/services/projects.ts @@ -15,7 +15,7 @@ import { WorkspaceNotFoundError } from '@/modules/workspaces/errors/workspace' import { GetProject, UpdateProject } from '@/modules/core/domain/projects/operations' -import { chunk } from 'lodash' +import { chunk } from 'lodash-es' import { Roles, WorkspaceRoles } from '@speckle/shared' import { GetStreamCollaborators, diff --git a/packages/server/modules/workspaces/tests/helpers/creation.ts b/packages/server/modules/workspaces/tests/helpers/creation.ts index 88205349f..d964dede2 100644 --- a/packages/server/modules/workspaces/tests/helpers/creation.ts +++ b/packages/server/modules/workspaces/tests/helpers/creation.ts @@ -110,7 +110,7 @@ import { getWorkspaceSeatTypeToProjectRoleMappingFactory, validateWorkspaceMemberProjectRoleFactory } from '@/modules/workspaces/services/projects' -import { assign, isBoolean, isString } from 'lodash' +import { assign, isBoolean, isString } from 'lodash-es' import { captureCreatedInvite } from '@/test/speckle-helpers/inviteHelper' import { finalizeInvitedServerRegistrationFactory, diff --git a/packages/server/modules/workspaces/tests/helpers/invites.ts b/packages/server/modules/workspaces/tests/helpers/invites.ts index cf21e9459..815cec0de 100644 --- a/packages/server/modules/workspaces/tests/helpers/invites.ts +++ b/packages/server/modules/workspaces/tests/helpers/invites.ts @@ -32,7 +32,7 @@ import { expect } from 'chai' import { MaybeAsync, StreamRoles, WorkspaceRoles } from '@speckle/shared' import { expectToThrow } from '@/test/assertionHelper' import { ForbiddenError } from '@/modules/shared/errors' -import { isBoolean } from 'lodash' +import { isBoolean } from 'lodash-es' import { WorkspaceSeatType } from '@/modules/workspacesCore/domain/types' export const buildInvitesGraphqlOperations = (deps: { apollo: TestApolloServer }) => { diff --git a/packages/server/modules/workspaces/tests/integration/invites.graph.spec.ts b/packages/server/modules/workspaces/tests/integration/invites.graph.spec.ts index d67be1ae9..e57a47a28 100644 --- a/packages/server/modules/workspaces/tests/integration/invites.graph.spec.ts +++ b/packages/server/modules/workspaces/tests/integration/invites.graph.spec.ts @@ -12,8 +12,6 @@ import { TestApolloServer } from '@/test/graphqlHelper' import { beforeEachContext, truncateTables } from '@/test/hooks' -import { describe } from 'mocha' -import { EmailSendingServiceMock } from '@/test/mocks/global' import { WorkspaceRole } from '@/test/graphql/generated/graphql' import { expect } from 'chai' import { @@ -24,7 +22,7 @@ import { Roles, StreamRoles, WorkspaceRoles } from '@speckle/shared' import { itEach } from '@/test/assertionHelper' import { ServerInvites } from '@/modules/core/dbSchema' import { TokenResourceIdentifierType } from '@/modules/core/graph/generated/graphql' -import { times } from 'lodash' +import { times } from 'lodash-es' import { findInviteFactory } from '@/modules/serverinvites/repositories/serverInvites' import { db } from '@/db/knex' import { @@ -72,6 +70,7 @@ import { getEventBus } from '@/modules/shared/services/eventBus' import { WorkspaceSeatType } from '@/modules/workspacesCore/domain/types' import { ProjectRecordVisibility } from '@/modules/core/helpers/types' import { getFeatureFlags } from '@/modules/shared/helpers/envHelper' +import { createEmailListener, TestEmailListener } from '@/test/speckle-helpers/email' enum InviteByTarget { Email = 'email', @@ -137,6 +136,8 @@ describe('Workspaces Invites GQL', () => { const workspaceDomain = 'example.org' + let emailListener: TestEmailListener + before(async () => { const ctx = await beforeEachContext() app = ctx.app @@ -164,10 +165,15 @@ describe('Workspaces Invites GQL', () => { [otherGuysWorkspace, me, Roles.Workspace.Member], [myFirstWorkspace, myWorkspaceFriend, Roles.Workspace.Member] ]) + emailListener = await createEmailListener() + }) + + after(async () => { + await emailListener.destroy() }) afterEach(() => { - EmailSendingServiceMock.resetMockedFunctions() + emailListener.reset() }) describe('when authenticated', () => { @@ -317,11 +323,7 @@ describe('Workspaces Invites GQL', () => { it('batch inviting works', async () => { const count = 10 - const sendEmailInvocations = EmailSendingServiceMock.hijackFunction( - 'sendEmail', - async () => true, - { times: count } - ) + const { getSends } = emailListener.listen({ times: count }) const res = await gqlHelpers.batchCreateInvites({ workspaceId: myFirstWorkspace.id, @@ -337,14 +339,11 @@ describe('Workspaces Invites GQL', () => { res.data?.workspaceMutations?.invites?.batchCreate?.invitedTeam ).to.have.length(count) - expect(sendEmailInvocations.args).to.have.lengthOf(count) + expect(getSends()).to.have.lengthOf(count) }) it('works when inviting user by id', async () => { - const sendEmailInvocations = EmailSendingServiceMock.hijackFunction( - 'sendEmail', - async () => true - ) + const { getSends } = emailListener.listen({ times: 2 }) const randomUnregisteredEmail = `${createRandomPassword()}@example.org` await createUserEmailFactory({ db })({ @@ -377,8 +376,9 @@ describe('Workspaces Invites GQL', () => { expect(workspace.invitedTeam![0].user?.id).to.equal(otherGuy.id) - expect(sendEmailInvocations.args).to.have.lengthOf(1) - const emailParams = sendEmailInvocations.args[0][0] + const emailSends = getSends() + expect(emailSends).to.have.lengthOf(1) + const emailParams = emailSends[0] expect(emailParams).to.be.ok expect(emailParams.to).to.eq(otherGuy.email) expect(emailParams.subject).to.be.ok @@ -387,10 +387,7 @@ describe('Workspaces Invites GQL', () => { await validateInviteExistanceFromEmail(emailParams) }) it('works when inviting user by email', async () => { - const sendEmailInvocations = EmailSendingServiceMock.hijackFunction( - 'sendEmail', - async () => true - ) + const { getSends } = emailListener.listen({ times: 2 }) const randomUnregisteredEmail = `${createRandomPassword()}@example.org` @@ -413,8 +410,9 @@ describe('Workspaces Invites GQL', () => { expect(workspace.invitedTeam![0].user).to.be.not.ok expect(workspace.invitedTeam![0].title).to.equal(randomUnregisteredEmail) - expect(sendEmailInvocations.args).to.have.lengthOf(1) - const emailParams = sendEmailInvocations.args[0][0] + const emailSends = getSends() + expect(emailSends).to.have.lengthOf(1) + const emailParams = emailSends[0] expect(emailParams).to.be.ok expect(emailParams.to).to.eq(randomUnregisteredEmail) expect(emailParams.subject).to.be.ok @@ -664,10 +662,7 @@ describe('Workspaces Invites GQL', () => { }) it('can invite to workspace project as admin, even if target doesnt belong to workspace', async () => { - const sendEmailInvocations = EmailSendingServiceMock.hijackFunction( - 'sendEmail', - async () => true - ) + const { getSends } = emailListener.listen({ times: 2 }) const res = await gqlHelpers.createWorkspaceProjectInvite({ projectId: myProjectInviteTargetWorkspaceProject.id, @@ -683,8 +678,9 @@ describe('Workspaces Invites GQL', () => { expect(res.data?.projectMutations.invites.createForWorkspace.id).to.be.ok // no auto-accept, since target is not a workspace member - expect(sendEmailInvocations.args).to.have.lengthOf(1) - const emailParams = sendEmailInvocations.args[0][0] + const emailSends = getSends() + expect(emailSends).to.have.lengthOf(1) + const emailParams = emailSends[0] await validateInviteExistanceFromEmail(emailParams) await gqlHelpers.validateResourceAccess({ @@ -718,10 +714,7 @@ describe('Workspaces Invites GQL', () => { }) it('invite auto-accepted if both users already belong to the workspace', async () => { - const sendEmailInvocations = EmailSendingServiceMock.hijackFunction( - 'sendEmail', - async () => true - ) + const { getSends } = emailListener.listen({ times: 2 }) const res = await gqlHelpers.createWorkspaceProjectInvite({ projectId: myProjectInviteTargetWorkspaceProject.id, @@ -737,7 +730,7 @@ describe('Workspaces Invites GQL', () => { expect(res.data?.projectMutations.invites.createForWorkspace.id).to.be.ok // No invite email should be sent out, due to auto-accept - expect(sendEmailInvocations.length()).to.eq(0) + expect(getSends().length).to.eq(0) // Should have project role await gqlHelpers.validateResourceAccess({ @@ -1291,10 +1284,7 @@ describe('Workspaces Invites GQL', () => { }) it('can resend the invite email', async () => { - const sendEmailInvocations = EmailSendingServiceMock.hijackFunction( - 'sendEmail', - async () => true - ) + const { getSends } = emailListener.listen({ times: 2 }) const res = await gqlHelpers.resendWorkspaceInvite({ input: { @@ -1306,8 +1296,9 @@ describe('Workspaces Invites GQL', () => { expect(res).to.not.haveGraphQLErrors() expect(res.data?.workspaceMutations.invites.resend).to.be.ok - expect(sendEmailInvocations.args).to.have.lengthOf(1) - const emailParams = sendEmailInvocations.args[0][0] + const emailSends = getSends() + expect(emailSends).to.have.lengthOf(1) + const emailParams = emailSends[0] expect(emailParams).to.be.ok expect(emailParams.to).to.eq(otherGuy.email) }) diff --git a/packages/server/modules/workspaces/tests/integration/projects.graph.spec.ts b/packages/server/modules/workspaces/tests/integration/projects.graph.spec.ts index a1a7d198c..d9126d99c 100644 --- a/packages/server/modules/workspaces/tests/integration/projects.graph.spec.ts +++ b/packages/server/modules/workspaces/tests/integration/projects.graph.spec.ts @@ -66,7 +66,7 @@ import { expect } from 'chai' import cryptoRandomString from 'crypto-random-string' import dayjs from 'dayjs' import { Knex } from 'knex' -import { times } from 'lodash' +import { times } from 'lodash-es' const grantStreamPermissions = grantStreamPermissionsFactory({ db }) const adminOverrideMock = mockAdminOverride() diff --git a/packages/server/modules/workspaces/tests/integration/regions.graph.spec.ts b/packages/server/modules/workspaces/tests/integration/regions.graph.spec.ts index e6db423b2..bad40af04 100644 --- a/packages/server/modules/workspaces/tests/integration/regions.graph.spec.ts +++ b/packages/server/modules/workspaces/tests/integration/regions.graph.spec.ts @@ -1,5 +1,6 @@ import { db } from '@/db/knex' import { isMultiRegionEnabled } from '@/modules/multiregion/helpers' +import { setMultiRegionConfig } from '@/modules/multiregion/regionConfig' import { storeRegionFactory } from '@/modules/multiregion/repositories' import { WorkspaceRegions } from '@/modules/workspaces/repositories/regions' import { @@ -15,9 +16,9 @@ import { } from '@/test/graphql/generated/graphql' import { testApolloServer, TestApolloServer } from '@/test/graphqlHelper' import { beforeEachContext, getRegionKeys } from '@/test/hooks' -import { MultiRegionDbSelectorMock } from '@/test/mocks/global' import { truncateRegionsSafely } from '@/test/speckle-helpers/regions' import { PaidWorkspacePlans, Roles } from '@speckle/shared' +import { getConnectionSettings } from '@speckle/shared/environment/db' import { expect } from 'chai' const storeRegion = storeRegionFactory({ db }) @@ -41,9 +42,21 @@ isEnabled let apollo: TestApolloServer before(async () => { - MultiRegionDbSelectorMock.mockFunction('getDb', async () => db) - MultiRegionDbSelectorMock.mockFunction('getRegionDb', async () => db) + // Faking multi region config + const connectionUri = getConnectionSettings(db).connectionString! + const region = { + postgres: { + connectionUri, + skipInitialization: true + } + } + setMultiRegionConfig({ + regions: { + [region1Key]: region, + [region2Key]: region + } + }) await beforeEachContext() me = await createTestUser({ role: Roles.Server.Admin }) @@ -79,7 +92,7 @@ isEnabled }) after(async () => { - MultiRegionDbSelectorMock.resetMockedFunctions() + setMultiRegionConfig(undefined) await truncateRegionsSafely() }) diff --git a/packages/server/modules/workspaces/tests/integration/repositories.spec.ts b/packages/server/modules/workspaces/tests/integration/repositories.spec.ts index d4dc47353..cb83136ae 100644 --- a/packages/server/modules/workspaces/tests/integration/repositories.spec.ts +++ b/packages/server/modules/workspaces/tests/integration/repositories.spec.ts @@ -50,7 +50,7 @@ import { grantStreamPermissionsFactory, upsertProjectRoleFactory } from '@/modules/core/repositories/streams' -import { omit } from 'lodash' +import { omit } from 'lodash-es' import { createAndStoreTestWorkspaceFactory } from '@/test/speckle-helpers/workspaces' import { WorkspaceJoinRequests } from '@/modules/workspacesCore/helpers/db' import { insertInviteAndDeleteOldFactory } from '@/modules/serverinvites/repositories/serverInvites' diff --git a/packages/server/modules/workspaces/tests/integration/repositories/users.spec.ts b/packages/server/modules/workspaces/tests/integration/repositories/users.spec.ts index 33cd0c079..a9ccbf0ea 100644 --- a/packages/server/modules/workspaces/tests/integration/repositories/users.spec.ts +++ b/packages/server/modules/workspaces/tests/integration/repositories/users.spec.ts @@ -12,7 +12,7 @@ import { import { BasicTestUser, createTestUser, createTestUsers } from '@/test/authHelper' import { BasicTestStream, createTestStream } from '@/test/speckle-helpers/streamHelper' import { expect } from 'chai' -import { pick } from 'lodash' +import { pick } from 'lodash-es' describe('Workspace repositories', () => { describe('users repository', () => { diff --git a/packages/server/modules/workspaces/tests/integration/workspaceSeat.graph.spec.ts b/packages/server/modules/workspaces/tests/integration/workspaceSeat.graph.spec.ts index 4fb43970e..86a61483c 100644 --- a/packages/server/modules/workspaces/tests/integration/workspaceSeat.graph.spec.ts +++ b/packages/server/modules/workspaces/tests/integration/workspaceSeat.graph.spec.ts @@ -1,8 +1,10 @@ +/* eslint-disable camelcase */ import { db } from '@/db/knex' import { createRandomEmail, createRandomString } from '@/modules/core/helpers/testHelpers' +import { setStripeClient } from '@/modules/gatekeeper/clients/stripe' import { WorkspaceSeatType } from '@/modules/gatekeeper/domain/billing' import { getWorkspaceUserSeatFactory } from '@/modules/gatekeeper/repositories/workspaceSeat' import { @@ -18,7 +20,6 @@ import { } from '@/test/graphql/generated/graphql' import { testApolloServer, TestApolloServer } from '@/test/graphqlHelper' import { beforeEachContext } from '@/test/hooks' -import { StripeClientMock } from '@/test/mocks/global' import { addToStream, BasicTestStream, @@ -26,6 +27,9 @@ import { } from '@/test/speckle-helpers/streamHelper' import { Roles } from '@speckle/shared' import { expect } from 'chai' +import dayjs from 'dayjs' +import type { Stripe } from 'stripe' +import { Mock, It, Times } from 'moq.ts' const getWorkspaceUserSeat = getWorkspaceUserSeatFactory({ db }) @@ -47,6 +51,12 @@ describe('Workspace Seats @graphql', () => { let apollo: TestApolloServer + let mockedStripe: Mock + let mockedStripeSubscriptions: Mock + let capturedStripeUpdateArgs: Array< + Parameters + > = [] + before(async () => { await beforeEachContext() await createTestUsers([workspaceAdmin, workspaceMember]) @@ -56,14 +66,38 @@ describe('Workspace Seats @graphql', () => { beforeEach(() => { // cause we have a fake subscription - StripeClientMock.mockFunction( - 'reconcileWorkspaceSubscriptionFactory', - () => async () => {} - ) + capturedStripeUpdateArgs = [] + mockedStripe = new Mock() + + const fakeStripeSubscription = { + customer: createRandomString(), + id: createRandomString(), + status: 'active', + cancel_at: null, + current_period_end: dayjs().add(1, 'month').unix(), + items: { data: [] } + } as unknown as Stripe.Response + + mockedStripeSubscriptions = new Mock() + mockedStripeSubscriptions + .setup((s) => s.retrieve(It.IsAny())) + .returnsAsync(fakeStripeSubscription) + .setup((s) => s.update(It.IsAny(), It.IsAny())) + .callback((args) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + capturedStripeUpdateArgs.push(args.args as any) + return Promise.resolve({} as unknown as Stripe.Response) + }) + + mockedStripe + .setup((s) => s.subscriptions) + .returns(mockedStripeSubscriptions.object()) + + setStripeClient(mockedStripe.object()) }) after(async () => { - StripeClientMock.resetMockedFunctions() + setStripeClient(undefined) }) describe('when being changed', () => { @@ -125,7 +159,7 @@ describe('Workspace Seats @graphql', () => { expect(res.data?.workspaceMutations.updateSeatType).to.not.be.ok }) - it.skip('should upgrade a workspace seat and reconcile subscription', async () => { + it('should upgrade a workspace seat and reconcile subscription', async () => { const user: BasicTestUser = { id: createRandomString(), name: createRandomString(), @@ -141,11 +175,6 @@ describe('Workspace Seats @graphql', () => { }) expect(oldSeat?.type).to.eq(WorkspaceSeatType.Viewer) - const { args, length: reconciledTimes } = StripeClientMock.hijackFactoryFunction( - 'reconcileWorkspaceSubscriptionFactory', - async () => {} - ) - const res = await updateSeatType({ workspaceId: testWorkspace1.id, userId: user.id, @@ -158,11 +187,16 @@ describe('Workspace Seats @graphql', () => { (i) => i.id === user.id )?.seatType ).to.eq(WorkspaceSeatType.Editor) - expect(reconciledTimes() > 0).to.be.true - const reconcileArgs = args[0][0] - expect(reconcileArgs.prorationBehavior).to.eq('always_invoice') // new plan - expect(reconcileArgs.subscriptionData.products.length).to.be.ok + // ensure update was called at least once + mockedStripeSubscriptions.verify( + (s) => s.update(It.IsAny(), It.IsAny()), + Times.AtLeastOnce() + ) + + expect(capturedStripeUpdateArgs).to.have.length.greaterThan(0) + const reconcileArgs = capturedStripeUpdateArgs.at(-1)! + expect(reconcileArgs[1]!.proration_behavior).to.eq('always_invoice') }) it('should downgrade a workspace seat', async () => { diff --git a/packages/server/modules/workspaces/tests/integration/workspaces.graph.spec.ts b/packages/server/modules/workspaces/tests/integration/workspaces.graph.spec.ts index 6a64a13c7..8973cc715 100644 --- a/packages/server/modules/workspaces/tests/integration/workspaces.graph.spec.ts +++ b/packages/server/modules/workspaces/tests/integration/workspaces.graph.spec.ts @@ -47,7 +47,7 @@ import { } from '@/modules/workspaces/tests/helpers/creation' import { BasicTestCommit, createTestCommit } from '@/test/speckle-helpers/commitHelper' import { BasicTestStream, createTestStream } from '@/test/speckle-helpers/streamHelper' -import { shuffle } from 'lodash' +import { shuffle } from 'lodash-es' import knex, { db } from '@/db/knex' import { createRandomPassword, diff --git a/packages/server/modules/workspaces/tests/unit/domain/logic.spec.ts b/packages/server/modules/workspaces/tests/unit/domain/logic.spec.ts index 129b35140..381f7cf2b 100644 --- a/packages/server/modules/workspaces/tests/unit/domain/logic.spec.ts +++ b/packages/server/modules/workspaces/tests/unit/domain/logic.spec.ts @@ -14,7 +14,7 @@ import { expectToThrow } from '@/test/assertionHelper' import { Roles } from '@speckle/shared' import { expect } from 'chai' import cryptoRandomString from 'crypto-random-string' -import { merge } from 'lodash' +import { merge } from 'lodash-es' const createTestEmail = ( emailInput?: Partial diff --git a/packages/server/modules/workspaces/tests/unit/services/management.spec.ts b/packages/server/modules/workspaces/tests/unit/services/management.spec.ts index be69dfc70..8a2332c5c 100644 --- a/packages/server/modules/workspaces/tests/unit/services/management.spec.ts +++ b/packages/server/modules/workspaces/tests/unit/services/management.spec.ts @@ -35,7 +35,7 @@ import { WorkspaceUnverifiedDomainError } from '@/modules/workspaces/errors/workspace' import { UserEmail } from '@/modules/core/domain/userEmails/types' -import { merge, omit } from 'lodash' +import { merge, omit } from 'lodash-es' import { GetWorkspaceWithDomains, UpsertWorkspaceArgs diff --git a/packages/server/modules/workspacesCore/graph/resolvers/workspacesCore.ts b/packages/server/modules/workspacesCore/graph/resolvers/workspacesCore.ts index db6594d2f..7adf2566d 100644 --- a/packages/server/modules/workspacesCore/graph/resolvers/workspacesCore.ts +++ b/packages/server/modules/workspacesCore/graph/resolvers/workspacesCore.ts @@ -9,7 +9,7 @@ import { WorkspaceDefaultSeatType } from '@/modules/workspacesCore/domain/consta const { FF_WORKSPACES_MODULE_ENABLED } = getFeatureFlags() -export = !FF_WORKSPACES_MODULE_ENABLED +export default !FF_WORKSPACES_MODULE_ENABLED ? ({ Query: { workspace: async () => { diff --git a/packages/server/modules/workspacesCore/migrations/20250514092509_add_missing_seats.ts b/packages/server/modules/workspacesCore/migrations/20250514092509_add_missing_seats.ts index acec6dca8..a7d8233ef 100644 --- a/packages/server/modules/workspacesCore/migrations/20250514092509_add_missing_seats.ts +++ b/packages/server/modules/workspacesCore/migrations/20250514092509_add_missing_seats.ts @@ -1,5 +1,5 @@ import { Knex } from 'knex' -import { chunk } from 'lodash' +import { chunk } from 'lodash-es' type WorkspaceAcl = { userId: string diff --git a/packages/server/nyc.config.js b/packages/server/nyc.config.js index e557e603a..0ce8a2587 100644 --- a/packages/server/nyc.config.js +++ b/packages/server/nyc.config.js @@ -1,6 +1,6 @@ const testFileExtensions = ['ts', 'js'] -module.exports = { +export default { exclude: [ `**/migrations/*.{${testFileExtensions}}`, `**/modules/cli/**/*.{${testFileExtensions}}`, diff --git a/packages/server/observability/components/express/expressLogging.ts b/packages/server/observability/components/express/expressLogging.ts index 626429402..1db8f1b9e 100644 --- a/packages/server/observability/components/express/expressLogging.ts +++ b/packages/server/observability/components/express/expressLogging.ts @@ -8,7 +8,7 @@ import type { GenReqId } from 'pino-http' import type { IncomingMessage, ServerResponse } from 'http' import { ensureError, type Optional } from '@speckle/shared' import { getRequestParameters, getRequestPath } from '@/modules/core/helpers/server' -import { get } from 'lodash' +import { get } from 'lodash-es' export const REQUEST_ID_HEADER = 'x-request-id' diff --git a/packages/server/observability/components/knex/knexMonitoring.ts b/packages/server/observability/components/knex/knexMonitoring.ts index 78f7cdbaf..443ad6687 100644 --- a/packages/server/observability/components/knex/knexMonitoring.ts +++ b/packages/server/observability/components/knex/knexMonitoring.ts @@ -3,7 +3,7 @@ import { numberOfFreeConnections } from '@/modules/shared/helpers/dbHelper' import { type Knex } from 'knex' import { Logger } from 'pino' import { toNDecimalPlaces } from '@/modules/core/utils/formatting' -import { omit } from 'lodash' +import { omit } from 'lodash-es' import { getRequestContext, isRequestContext, diff --git a/packages/server/observability/utils/logLevels.ts b/packages/server/observability/utils/logLevels.ts index 67794d570..2994a3603 100644 --- a/packages/server/observability/utils/logLevels.ts +++ b/packages/server/observability/utils/logLevels.ts @@ -1,8 +1,10 @@ import { BaseError } from '@/modules/shared/errors' -import { isUserGraphqlError } from '@/modules/shared/helpers/graphqlHelper' -import { ApolloError } from '@apollo/client/core/core.cjs' +import { + isGraphQLError, + isUserGraphqlError +} from '@/modules/shared/helpers/graphqlHelper' +import { ApolloError } from '@apollo/client/core' import { ensureError } from '@speckle/shared' -import { GraphQLError } from 'graphql' import type { Logger } from 'pino' interface LogFn { (logger: Logger, e: unknown, obj?: unknown, msg?: string, ...args: unknown[]): void @@ -24,7 +26,7 @@ export const logWithErr: LogFn = (logger, e, obj, msg?, ...args) => { } export const shouldLogAsInfoLevel = (err: unknown): boolean => { - if (err instanceof GraphQLError) { + if (isGraphQLError(err)) { if (isUserGraphqlError(err)) return true if (err.message === 'Connection is closed.') return true if (!!err.cause && shouldLogAsInfoLevel(err.cause)) return true @@ -43,7 +45,7 @@ export const shouldLogAsInfoLevel = (err: unknown): boolean => { } const shouldLogAsWarnLevel = (err: unknown): boolean => { - if (!(err instanceof GraphQLError)) return false + if (!isGraphQLError(err)) return false if (err.message.startsWith('Cannot return null for non-nullable field')) return true if ( diff --git a/packages/server/package.json b/packages/server/package.json index 7da6a243c..d76381f7f 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -7,41 +7,41 @@ "author": "AEC Systems", "license": "SEE LICENSE IN readme.md", "main": "./bin/www", - "type": "commonjs", "repository": { "type": "git", "url": "https://github.com/specklesystems/Server.git" }, + "type": "module", "engines": { "node": "^22.6.0" }, "scripts": { "build": "tsc -p ./tsconfig.build.json", "build:watch": "tsc -p ./tsconfig.build.json -w", - "run:watch": "cross-env NODE_ENV=development LOG_PRETTY=true nodemon ./bin/www --signal SIGKILL --watch ./dist --watch ./assets --watch ./bin/www --watch .env --watch multiregion.json -e js,ts,graphql,env,gql", + "run:watch": "cross-env NODE_ENV=development LOG_PRETTY=true nodemon --signal SIGKILL --import=./esmLoader.js ./bin/www --watch ./dist --watch ./assets --watch ./bin/www --watch .env --watch multiregion.json -e js,ts,graphql,env,gql", "dev": "concurrently \"npm:build:watch\" \"npm:run:watch\" \"yarn gqlgen:watch\" -n tsc,server,gqlgen", "build:clean": "rimraf ./dist && yarn build", "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", - "ts-mocha": "node --require ts-node/register ./bin/mocha", - "test": "cross-env NODE_ENV=test LOG_LEVEL=silent LOG_PRETTY=true yarn ts-mocha", + "ts-mocha": "tsx ./bin/mocha", + "ts-gqlgen": "tsx ./bin/gqlgen", + "test": "cross-env NODE_ENV=test LOG_FILTER=test LOG_PRETTY=true yarn ts-mocha", "test:all-ff": "cross-env ENABLE_ALL_FFS=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 --grep @multiregion", "test:no-ff": "cross-env DISABLE_ALL_FFS=true yarn test", - "test:coverage": "cross-env NODE_ENV=test LOG_LEVEL=silent LOG_PRETTY=true nyc --reporter lcov yarn ts-mocha", + "test:coverage": "cross-env NODE_ENV=test LOG_FILTER=test LOG_PRETTY=true nyc --reporter lcov yarn ts-mocha", "test:report": "MOCHA_FILE=reports/test-results.xml yarn test:coverage -- --reporter mocha-multi --reporter-options spec=-,mocha-junit-reporter=reports/test-results.xml", "lint": "yarn lint:tsc && yarn lint:eslint", "lint:ci": "yarn lint:tsc", "lint:tsc": "tsc --noEmit", "lint:eslint": "eslint .", - "cli": "cross-env LOG_LEVEL=debug LOG_PRETTY=true NODE_ENV=development ts-node ./modules/cli/index.ts", - "cli:test": "cross-env LOG_LEVEL=debug LOG_PRETTY=true NODE_ENV=test ts-node ./modules/cli/index.ts", + "cli": "cross-env LOG_LEVEL=debug LOG_PRETTY=true NODE_ENV=development tsx ./modules/cli/index.ts", + "cli:test": "cross-env LOG_LEVEL=debug LOG_PRETTY=true NODE_ENV=test tsx ./modules/cli/index.ts", "cli:test:multiregion": "cross-env RUN_TESTS_IN_MULTIREGION_MODE=true yarn cli:test", "cli:download:commit": "cross-env LOG_PRETTY=true LOG_LEVEL=debug yarn cli download commit", "migrate": "yarn cli db migrate", - "migrate:test": "cross-env NODE_ENV=test ts-node ./modules/cli/index.js db migrate", - "gqlgen": "graphql-codegen --config codegen.yml", - "gqlgen:watch": "graphql-codegen --config codegen.yml --watch \"assets/**/*.graphql\"", + "migrate:test": "cross-env NODE_ENV=test tsx ./modules/cli/index.js db migrate", + "gqlgen": "yarn ts-gqlgen --config codegen.ts", + "gqlgen:watch": "yarn gqlgen --watch", "stripe:listen": "stripe listen --forward-to 127.0.0.1:3000/api/v1/billing/webhooks" }, "dependencies": { @@ -88,9 +88,10 @@ "express-async-errors": "^3.1.1", "express-prom-bundle": "^6.6.0", "express-session": "^1.17.1", + "extensionless": "^1.9.9", "graphql": "^16.6.0", "graphql-redis-subscriptions": "^2.2.2", - "graphql-scalars": "^1.18.0", + "graphql-scalars": "^1.24.1", "graphql-subscriptions": "^2.0.0", "graphql-tag": "^2.12.6", "ioredis": "^5.2.2", @@ -99,6 +100,7 @@ "knex": "^2.5.1", "libsodium-wrappers": "^0.7.13", "lodash": "^4.17.21", + "lodash-es": "^4.17.21", "lru-cache": "^11.0.1", "mixpanel": "^0.17.0", "mjml": "^4.13.0", @@ -142,13 +144,12 @@ "devDependencies": { "@apollo/rover": "^0.23.0", "@faker-js/faker": "^8.4.1", - "@graphql-codegen/cli": "^5.0.5", - "@graphql-codegen/typed-document-node": "^5.1.1", + "@graphql-codegen/cli": "^5.0.7", + "@graphql-codegen/typed-document-node": "^5.1.2", "@graphql-codegen/typescript": "^4.1.6", - "@graphql-codegen/typescript-operations": "^4.6.0", - "@graphql-codegen/typescript-resolvers": "^4.5.0", + "@graphql-codegen/typescript-operations": "^4.6.1", + "@graphql-codegen/typescript-resolvers": "^4.5.1", "@parcel/watcher": "^2.5.1", - "@swc/core": "^1.11.11", "@tiptap/core": "^2.0.0-beta.176", "@types/bcrypt": "^5.0.0", "@types/bull": "^3.15.9", @@ -159,6 +160,7 @@ "@types/content-disposition": "^0.5.9", "@types/cookie-parser": "^1.4.7", "@types/cors": "^2.8.17", + "@types/deasync": "^0", "@types/debug": "^4.1.7", "@types/deep-equal-in-any-order": "^1.0.1", "@types/ejs": "^3.1.1", @@ -166,10 +168,10 @@ "@types/ioredis-mock": "^8.2.5", "@types/libsodium-wrappers": "^0", "@types/lodash": "^4.14.180", + "@types/lodash-es": "^4.17.12", "@types/mailchimp__mailchimp_marketing": "^3.0.9", "@types/mjml": "^4.7.0", "@types/mocha": "^10.0.0", - "@types/mock-require": "^2.0.1", "@types/module-alias": "^2.0.1", "@types/netmask": "^2.0.0", "@types/node": "^18.19.38", @@ -189,7 +191,7 @@ "@types/uuid": "^9.0.0", "@types/verror": "^1.10.6", "@types/xml-escape": "^1.1.3", - "@types/yargs": "^17.0.10", + "@types/yargs": "^17.0.33", "@types/zxcvbn": "^4.4.5", "@typescript-eslint/eslint-plugin": "^5.39.0", "@typescript-eslint/parser": "^5.39.0", @@ -200,6 +202,7 @@ "concurrently": "^7.0.0", "cross-env": "^7.0.3", "csv-parse": "^5.6.0", + "deasync": "^0.1.30", "deep-equal-in-any-order": "^1.1.15", "enforce-unique": "^1.3.0", "eslint": "^8.11.0", @@ -209,21 +212,22 @@ "mocha": "^10.1.0", "mocha-junit-reporter": "^2.0.2", "mocha-multi": "1.1.7", - "mock-require": "^3.0.3", "mock-socket": "^9.3.1", + "moq.ts": "10.0.8", "node-mocks-http": "^1.12.1", "nodemon": "^3.1.9", "nyc": "^15.0.1", "prettier": "^2.5.1", "rimraf": "^5.0.7", "supertest": "^4.0.2", - "ts-node": "^10.9.2", "tsconfig-paths": "^4.0.0", + "tsx": "^4.19.4", "type-fest": "^4.26.1", "typescript": "^4.6.4", "typescript-eslint": "^7.12.0", + "why-is-node-running": "^3.2.2", "ws": "^8.17.1", - "yargs": "^17.3.1" + "yargs": "^18.0.0" }, "config": { "commitizen": { diff --git a/packages/server/root.js b/packages/server/root.js new file mode 100644 index 000000000..d6b29fe64 --- /dev/null +++ b/packages/server/root.js @@ -0,0 +1,16 @@ +import path from 'node:path' +import { fileURLToPath } from 'url' + +// Conditionally change appRoot and packageRoot according to whether we're running from /dist/ or not (ts-node) +const isTsNode = + !!process[Symbol.for('ts-node.register.instance')] || + process.env.VITEST === 'true' || + (process._preload_modules || []).some((m) => m.match(/node_modules\/tsx\//)) // tsx running + +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) + +const packageRoot = __dirname // we know this file is located in the package root +const appRoot = isTsNode ? packageRoot : path.resolve(packageRoot, 'dist') + +export { appRoot, packageRoot } diff --git a/packages/server/scripts/duplicateUserMigration.js b/packages/server/scripts/duplicateUserMigration.ts similarity index 76% rename from packages/server/scripts/duplicateUserMigration.js rename to packages/server/scripts/duplicateUserMigration.ts index eb6f10104..9017e2820 100644 --- a/packages/server/scripts/duplicateUserMigration.js +++ b/packages/server/scripts/duplicateUserMigration.ts @@ -1,12 +1,12 @@ -const { knex } = require('@/db/knex') -const { logger } = require('@/observability/logging') -const roles = require('@/modules/core/roles.js') -const { Roles } = require('@speckle/shared') +import { knex } from '@/db/knex' +import { logger } from '@/observability/logging' +import roles from '@/modules/core/roles.js' +import { Roles } from '@speckle/shared' const Users = () => knex('users') // tableName, columnName that need migration -const migrationTargets = [ +const migrationTargets = [ ['api_tokens', 'owner'], ['authorization_codes', 'userId'], ['branches', 'authorId'], @@ -21,7 +21,15 @@ const migrationTargets = [ ['stream_activity', 'userId'] ] -const migrateColumnValue = async (tableName, columnName, oldUser, newUser) => { +type User = { id: string } +type StreamAcl = { userId: string; resourceId: string; role: string } + +const migrateColumnValue = async ( + tableName: string, + columnName: string, + oldUser: User, + newUser: User +) => { try { const query = knex(tableName) .where({ [columnName]: oldUser.id }) @@ -33,7 +41,13 @@ const migrateColumnValue = async (tableName, columnName, oldUser, newUser) => { } } -const serverAclMigration = async ({ lowerUser, upperUser }) => { +const serverAclMigration = async ({ + lowerUser, + upperUser +}: { + lowerUser: User + upperUser: User +}) => { const oldAcl = await knex('server_acl').where({ userId: upperUser.id }).first() // if the old user was admin, make the target admin too if (oldAcl.role === Roles.Server.Admin) @@ -42,7 +56,13 @@ const serverAclMigration = async ({ lowerUser, upperUser }) => { .update({ role: Roles.Server.Admin }) } -const _migrateSingleStreamAccess = async ({ lowerUser, upperStreamAcl }) => { +const _migrateSingleStreamAccess = async ({ + lowerUser, + upperStreamAcl +}: { + lowerUser: User + upperStreamAcl: StreamAcl +}) => { const upperRole = roles.filter((r) => r.name === upperStreamAcl.role)[0] const lowerAcl = await knex('stream_acl') .where({ userId: lowerUser.id, resourceId: upperStreamAcl.resourceId }) @@ -63,7 +83,13 @@ const _migrateSingleStreamAccess = async ({ lowerUser, upperStreamAcl }) => { } } -const streamAclMigration = async ({ lowerUser, upperUser }) => { +const streamAclMigration = async ({ + lowerUser, + upperUser +}: { + lowerUser: User + upperUser: User +}) => { const upperAcl = await knex('stream_acl').where({ userId: upperUser.id }) await Promise.all( @@ -74,17 +100,24 @@ const streamAclMigration = async ({ lowerUser, upperUser }) => { ) } -const createMigrations = ({ lowerUser, upperUser }) => +const createMigrations = ({ + lowerUser, + upperUser +}: { + lowerUser: User + upperUser: User +}) => migrationTargets.map(([tableName, columnName]) => { - migrateColumnValue(tableName, columnName, upperUser, lowerUser) + void migrateColumnValue(tableName, columnName, upperUser, lowerUser) }) -const userByEmailQuery = (email) => Users().where({ email }) +const userByEmailQuery = (email: string) => Users().where({ email }) const getDuplicateUsers = async () => { - const duplicates = await knex.raw( + const duplicates = (await knex.raw( 'select lower(email) as lowered, count(id) as reg_count from users group by lowered having count(id) > 1' - ) + )) as { rows: Array<{ lowered: string; reg_count: number }> } + return await Promise.all( duplicates.rows.map(async (dup) => { const lowerEmail = dup.lowered @@ -122,7 +155,7 @@ const runMigrations = async () => { ) } -;(async function () { +void (async function () { try { // await createData() await runMigrations() diff --git a/packages/server/scripts/moveProjectsBetweenServers.ts b/packages/server/scripts/moveProjectsBetweenServers.ts index a1bcedaab..f6fae3ddb 100644 --- a/packages/server/scripts/moveProjectsBetweenServers.ts +++ b/packages/server/scripts/moveProjectsBetweenServers.ts @@ -1,5 +1,5 @@ // eslint-disable-next-line no-restricted-imports -import '../bootstrap' +import '../bootstrap.js' import { configureClient } from '@/knexfile' import { @@ -48,7 +48,7 @@ import { import { retry } from '@lifeomic/attempt' import { Roles, StreamRoles } from '@speckle/shared' import knex from 'knex' -import { omit } from 'lodash' +import { omit } from 'lodash-es' const projectIds = [ 'edbf5f099d' diff --git a/packages/server/scripts/seedUsers.js b/packages/server/scripts/seedUsers.js deleted file mode 100644 index b7149f9ac..000000000 --- a/packages/server/scripts/seedUsers.js +++ /dev/null @@ -1,80 +0,0 @@ -require('../bootstrap') -const { db } = require('@/db/knex') -const { logger } = require('@/observability/logging') -const { getServerInfoFactory } = require('@/modules/core/repositories/server') -const { - findEmailFactory, - createUserEmailFactory, - ensureNoPrimaryEmailForUserFactory -} = require('@/modules/core/repositories/userEmails') -const { - getUserFactory, - storeUserFactory, - countAdminUsersFactory, - storeUserAclFactory -} = require('@/modules/core/repositories/users') -const { - validateAndCreateUserEmailFactory -} = require('@/modules/core/services/userEmails') -const { createUserFactory } = require('@/modules/core/services/users/management') -const { - deleteOldAndInsertNewVerificationFactory -} = require('@/modules/emails/repositories') -const { renderEmail } = require('@/modules/emails/services/emailRendering') -const { sendEmail } = require('@/modules/emails/services/sending') -const { - requestNewEmailVerificationFactory -} = require('@/modules/emails/services/verification/request') -const { - deleteServerOnlyInvitesFactory, - updateAllInviteTargetsFactory -} = require('@/modules/serverinvites/repositories/serverInvites') -const { - finalizeInvitedServerRegistrationFactory -} = require('@/modules/serverinvites/services/processing') -const { getEventBus } = require('@/modules/shared/services/eventBus') -const axios = require('axios').default - -const getServerInfo = getServerInfoFactory({ db }) -const findEmail = findEmailFactory({ db }) -const requestNewEmailVerification = requestNewEmailVerificationFactory({ - findEmail, - getUser: getUserFactory({ db }), - getServerInfo, - deleteOldAndInsertNewVerification: deleteOldAndInsertNewVerificationFactory({ db }), - renderEmail, - sendEmail -}) -const createUser = createUserFactory({ - getServerInfo, - findEmail, - storeUser: storeUserFactory({ db }), - countAdminUsers: countAdminUsersFactory({ db }), - storeUserAcl: storeUserAclFactory({ db }), - validateAndCreateUserEmail: validateAndCreateUserEmailFactory({ - createUserEmail: createUserEmailFactory({ db }), - ensureNoPrimaryEmailForUser: ensureNoPrimaryEmailForUserFactory({ db }), - findEmail, - updateEmailInvites: finalizeInvitedServerRegistrationFactory({ - deleteServerOnlyInvites: deleteServerOnlyInvitesFactory({ db }), - updateAllInviteTargets: updateAllInviteTargetsFactory({ db }) - }), - requestNewEmailVerification - }), - emitEvent: getEventBus().emit -}) - -const main = async () => { - const userInputs = ( - await axios.get('https://randomuser.me/api/?results=250') - ).data.results.map((user) => { - return { - name: `${user.name.first} ${user.name.last}`, - email: user.email, - password: `${user.login.password}${user.login.password}` - } - }) - await Promise.all(userInputs.map((userInput) => createUser(userInput))) -} - -main().then(logger.info('created')).catch(logger.error('failed')) diff --git a/packages/server/scripts/seedUsers.ts b/packages/server/scripts/seedUsers.ts new file mode 100644 index 000000000..2f21b6f1e --- /dev/null +++ b/packages/server/scripts/seedUsers.ts @@ -0,0 +1,82 @@ +/* eslint-disable no-restricted-imports */ +import '../bootstrap.js' +import { db } from '@/db/knex' +import { logger } from '@/observability/logging' +import { getServerInfoFactory } from '@/modules/core/repositories/server' +import { + findEmailFactory, + createUserEmailFactory, + ensureNoPrimaryEmailForUserFactory +} from '@/modules/core/repositories/userEmails' +import { + getUserFactory, + storeUserFactory, + countAdminUsersFactory, + storeUserAclFactory +} from '@/modules/core/repositories/users' +import { validateAndCreateUserEmailFactory } from '@/modules/core/services/userEmails' +import { createUserFactory } from '@/modules/core/services/users/management' +import { deleteOldAndInsertNewVerificationFactory } from '@/modules/emails/repositories' +import { renderEmail } from '@/modules/emails/services/emailRendering' +import { sendEmail } from '@/modules/emails/services/sending' +import { requestNewEmailVerificationFactory } from '@/modules/emails/services/verification/request' +import { + deleteServerOnlyInvitesFactory, + updateAllInviteTargetsFactory +} from '@/modules/serverinvites/repositories/serverInvites' +import { finalizeInvitedServerRegistrationFactory } from '@/modules/serverinvites/services/processing' +import { getEventBus } from '@/modules/shared/services/eventBus' +import axios from 'axios' + +const getServerInfo = getServerInfoFactory({ db }) +const findEmail = findEmailFactory({ db }) +const requestNewEmailVerification = requestNewEmailVerificationFactory({ + findEmail, + getUser: getUserFactory({ db }), + getServerInfo, + deleteOldAndInsertNewVerification: deleteOldAndInsertNewVerificationFactory({ db }), + renderEmail, + sendEmail +}) +const createUser = createUserFactory({ + getServerInfo, + findEmail, + storeUser: storeUserFactory({ db }), + countAdminUsers: countAdminUsersFactory({ db }), + storeUserAcl: storeUserAclFactory({ db }), + validateAndCreateUserEmail: validateAndCreateUserEmailFactory({ + createUserEmail: createUserEmailFactory({ db }), + ensureNoPrimaryEmailForUser: ensureNoPrimaryEmailForUserFactory({ db }), + findEmail, + updateEmailInvites: finalizeInvitedServerRegistrationFactory({ + deleteServerOnlyInvites: deleteServerOnlyInvitesFactory({ db }), + updateAllInviteTargets: updateAllInviteTargetsFactory({ db }) + }), + requestNewEmailVerification + }), + emitEvent: getEventBus().emit +}) + +const main = async () => { + const userInputs: Array[0]> = ( + await axios.get('https://randomuser.me/api/?results=250') + ).data.results.map( + (user: { + name: { first: string; last: string } + email: string + login: { password: string } + }) => { + return { + name: `${user.name.first} ${user.name.last}`, + email: user.email, + password: `${user.login.password}${user.login.password}` + } + } + ) + + await Promise.all(userInputs.map((userInput) => createUser(userInput))) +} + +void main() + .then(() => logger.info('created')) + .catch((e) => logger.error(e, 'failed')) diff --git a/packages/server/test/assertionHelper.ts b/packages/server/test/assertionHelper.ts index f76c0df05..9cd83d11e 100644 --- a/packages/server/test/assertionHelper.ts +++ b/packages/server/test/assertionHelper.ts @@ -1,7 +1,6 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { MaybeAsync, ensureError } from '@speckle/shared' import { AssertionError } from 'chai' -import { it } from 'mocha' export const expectToThrow = async (fn: () => MaybeAsync) => { try { diff --git a/packages/server/test/authHelper.ts b/packages/server/test/authHelper.ts index 4e5943d78..fe07edf17 100644 --- a/packages/server/test/authHelper.ts +++ b/packages/server/test/authHelper.ts @@ -38,7 +38,7 @@ import { createTestContext, testApolloServer } from '@/test/graphqlHelper' import { faker } from '@faker-js/faker' import { ServerScope, wait } from '@speckle/shared' import cryptoRandomString from 'crypto-random-string' -import { assign, isArray, isNumber, omit, times } from 'lodash' +import { assign, isArray, isNumber, omit, times } from 'lodash-es' const getServerInfo = getServerInfoFactory({ db }) const findEmail = findEmailFactory({ db }) diff --git a/packages/server/test/graphqlHelper.ts b/packages/server/test/graphqlHelper.ts index ca60b05be..46a4a1ab1 100644 --- a/packages/server/test/graphqlHelper.ts +++ b/packages/server/test/graphqlHelper.ts @@ -19,7 +19,7 @@ import { expect } from 'chai' import { ApolloServer, GraphQLResponse } from '@apollo/server' import { getUserFactory } from '@/modules/core/repositories/users' import { db } from '@/db/knex' -import { get, pick, set } from 'lodash' +import { get, pick, set } from 'lodash-es' import { isTestEnv } from '@/modules/shared/helpers/envHelper' import { publish, TestSubscriptions } from '@/modules/shared/utils/subscriptions' import cryptoRandomString from 'crypto-random-string' @@ -28,11 +28,12 @@ import type ws from 'ws' import { createAuthTokenForUser } from '@/test/authHelper' import { SubscriptionClient } from 'subscriptions-transport-ws' import { WebSocketLink } from '@apollo/client/link/ws/ws.cjs' -import { execute } from '@apollo/client/core/core.cjs' +import { execute } from '@apollo/client/core' import { PingPongDocument } from '@/test/graphql/generated/graphql' import { BaseError } from '@/modules/shared/errors' import EventEmitter from 'eventemitter2' import { expectToThrow } from '@/test/assertionHelper' +import { testLogger } from '@/observability/logging' type TypedGraphqlResponse> = GraphQLResponse @@ -295,7 +296,7 @@ export const testApolloSubscriptionServer = async () => { set(mockWsServer, 'removeListener', mockWsServer.off.bind(mockWsServer)) // backwards compat w/ subscriptions-transport-ws const mockWs = MockSocket.WebSocket as unknown as ws.WebSocket - const apolloSubServer = buildApolloSubscriptionServer({ server: mockWsServer }) + const apolloSubServer = await buildApolloSubscriptionServer({ server: mockWsServer }) // weakRef to ensure we dont prevent garbage collection const clients: WeakRef[] = [] @@ -357,26 +358,32 @@ export const testApolloSubscriptionServer = async () => { query, variables }) - const sub = observable.subscribe(async (eventData) => { - const res = eventData as FormattedExecutionResult - const asyncHandler = async () => handler(res) + const sub = observable.subscribe( + async (eventData) => { + const res = eventData as FormattedExecutionResult + const asyncHandler = async () => handler(res) - // Invoke handler - try { - await asyncHandler() - } catch (e) { - // If we throw here, this will be an unhandled rejection, lets throw in waitForMsg instead - eventBus.emit('error', e) - } + // Invoke handler + try { + await asyncHandler() + } catch (e) { + // If we throw here, this will be an unhandled rejection, lets throw in waitForMsg instead + eventBus.emit('error', e) + } - // Mark msg received - try { - messages.push(res) - await eventBus.emitAsync('message', res) - } catch (e) { - eventBus.emit('error', e) + // Mark msg received + try { + messages.push(res) + await eventBus.emitAsync('message', res) + } catch (e) { + eventBus.emit('error', e) + } + }, + (e) => { + errHandler(e) + testLogger.error(e, 'Test subscription subscribe error handler hit') } - }) + ) /** * Unsubscribe from the subscription diff --git a/packages/server/test/helpers.ts b/packages/server/test/helpers.ts index f0eeda5c3..46c429543 100644 --- a/packages/server/test/helpers.ts +++ b/packages/server/test/helpers.ts @@ -1,5 +1,5 @@ import crypto from 'crypto' -import { get } from 'lodash' +import { get } from 'lodash-es' /** * Generates an object containing the base object and an array of objects with an id. The base object will have a closure property which references all the other objects. diff --git a/packages/server/test/hooks.ts b/packages/server/test/hooks.ts index c4f95cfe0..b482f70c3 100644 --- a/packages/server/test/hooks.ts +++ b/packages/server/test/hooks.ts @@ -1,17 +1,17 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -// eslint-disable-next-line no-restricted-imports -import '../bootstrap' +/* eslint-disable no-restricted-imports */ +import '../bootstrap.js' // Register global mocks as early as possible import '@/test/mocks/global' -import chai from 'chai' import chaiAsPromised from 'chai-as-promised' import chaiHttp from 'chai-http' import deepEqualInAnyOrder from 'deep-equal-in-any-order' -import { knex as mainDb } from '@/db/knex' -import { init, startHttp, shutdown } from '@/app' import graphqlChaiPlugin from '@/test/plugins/graphql' +import { knex as mainDb } from '@/db/knex' +import chai from 'chai' +import { init, startHttp, shutdown } from '@/app' import { testLogger as logger } from '@/observability/logging' import { once } from 'events' import type http from 'http' @@ -22,12 +22,10 @@ import { MaybeAsync, MaybeNullOrUndefined, Nullable, - Optional, retry, TIME_MS, wait } from '@speckle/shared' -import * as mocha from 'mocha' import { getAvailableRegionKeysFactory, getFreeRegionKeysFactory @@ -50,15 +48,10 @@ import { isMultiRegionEnabled } from '@/modules/multiregion/helpers' import { GraphQLContext } from '@/modules/shared/helpers/typeHelper' import { ApolloServer } from '@apollo/server' import { ReadinessHandler } from '@/healthchecks/types' -import { set } from 'lodash' +import { set } from 'lodash-es' import { fixStackTrace } from '@/test/speckle-helpers/error' import { EnvironmentResourceError } from '@/modules/shared/errors' - -// why is server config only created once!???? -// because its done in a migration, to not override existing configs -// similarly wiping regions will break multi region setup -const protectedTables = ['server_config', 'regions'] -let regionClients: Record = {} +import * as mocha from 'mocha' // Register chai plugins chai.use(chaiAsPromised) @@ -66,6 +59,12 @@ chai.use(chaiHttp) chai.use(deepEqualInAnyOrder) chai.use(graphqlChaiPlugin) +// why is server config only created once!???? +// because its done in a migration, to not override existing configs +// similarly wiping regions will break multi region setup +const protectedTables = ['server_config', 'regions'] +let regionClients: Record = {} + // Please forgive me god for what I'm about to do, but Mocha's ancient API sucks ass // and there's NO OTHER WAY to format errors across all reporters const originalMochaRun = mocha.default.prototype.run @@ -350,36 +349,55 @@ export const initializeTestServer = async (params: { } } -let graphqlServer: Optional> = undefined - -export const mochaHooks: mocha.RootHookObject = { - beforeAll: async () => { - if (isMultiRegionTestMode()) { - logger.info('Running tests in multi-region mode...') - } - - logger.info('running before all') - - // Init (or cleanup) test databases - await setupDatabases() - - // Init app - ;({ graphqlServer } = await init()) - }, - afterAll: async () => { - logger.info('running after all') - await inEachDb(async (db) => { - await unlockFactory({ db })() - }) - await shutdown({ graphqlServer }) - } -} +let builtApps: Array>> = [] export const buildApp = async () => { - return await init() + const ret = await init() + builtApps.push(ret) + return ret } export const beforeEachContext = async () => { await truncateTables(undefined, { resetPubSub: true }) return await buildApp() } + +export const shutdownAll = async () => { + await Promise.all( + builtApps.map(async ({ graphqlServer, server, subscriptionServer }) => { + await graphqlServer.stop() + server.closeAllConnections() + subscriptionServer.close() + }) + ) + builtApps = [] + await shutdown({ graphqlServer: undefined }) +} + +export const beforeEntireTestRun = async () => { + if (isMultiRegionTestMode()) { + logger.info('Running tests in multi-region mode...') + } + + logger.info('🔧 Global setup: runs once before all tests') + + // Init (or cleanup) test databases + await setupDatabases() + + // Init app + await buildApp() +} + +export const afterEntireTestRun = async () => { + logger.info('🧹 Global teardown: runs once after all tests') + + await inEachDb(async (db) => { + await unlockFactory({ db })() + }) + await shutdownAll() +} + +export const mochaHooks: mocha.RootHookObject = { + beforeAll: beforeEntireTestRun, + afterAll: afterEntireTestRun +} diff --git a/packages/server/test/mockHelper.ts b/packages/server/test/mockHelper.ts deleted file mode 100644 index 320b1bcb6..000000000 --- a/packages/server/test/mockHelper.ts +++ /dev/null @@ -1,281 +0,0 @@ -/* eslint-disable @typescript-eslint/no-unsafe-return */ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import { Factory, FactoryResultOf } from '@/modules/shared/helpers/factory' -import { MaybeAsync } from '@/modules/shared/helpers/typeHelper' -import { isArray, isFunction } from 'lodash' -import mock from 'mock-require' -import { ConditionalPick } from 'type-fest' - -export type MockedFunctionImplementation = (...args: any[]) => MaybeAsync - -/** - * Mock a module's exported functions with the possibility to conditionally disable & change the mock - * @param {string|string[]} modulePaths Absolute & relative paths to the module being mocked or if you sometimes require - * with the '/index' suffix and sometimes don't you need to specify both options. Multiple options are required - * because of a limitation of mock-require - it doesn't understand that all of these point to the same thing. - * @param {string|string[]} dependencyPaths Paths to modules that use the mocked module and that you - * want to re-require so that if they are already loaded in memory, they're re-required with the new mock. Basically, - * if you've mocked a module, but it's not being used, debug the test and see if the mocked module is maybe required - * by another module that you haven't specified in this list. - */ -export function mockRequireModule< - MockType extends Record = Record ->( - modulePaths: string | string[], - dependencyPaths: string | string[] = [], - params: { preventDestroy?: boolean } = {} -) { - type MockTypeFunctionsOnly = ConditionalPick - type MockTypeFunctionProp = keyof MockTypeFunctionsOnly - - type MockTypeFactoriesOnly = ConditionalPick - type MockTypeFactoryProp = keyof MockTypeFactoriesOnly - - type MockedFunc = ( - ...args: Parameters - ) => ReturnType - - const { preventDestroy } = params - modulePaths = isArray(modulePaths) ? modulePaths : [modulePaths] - dependencyPaths = isArray(dependencyPaths) ? dependencyPaths : [dependencyPaths] - - let isDisabled = false - let functionReplacements: Partial< - Record> - > = {} - - const originalModule = require(modulePaths[0]) as MockType - const mockDefinition = new Proxy(originalModule, { - get(target, prop) { - const realProp = prop as keyof MockTypeFunctionsOnly - const propVal = target[realProp] - - if (!isFunction(propVal)) return propVal - return function (this: unknown, ...args: Parameters[]) { - const potentialReplacement = functionReplacements[realProp] as typeof propVal - if (isDisabled || !potentialReplacement || !isFunction(potentialReplacement)) { - return propVal.apply(this, args) - } - - return potentialReplacement.apply(this, args) - } - } - }) - - // Initialize mock with all paths (relative path, absolute alias path - both need to be specified - // cause of a limitation in mock-require) - for (const modulePath of modulePaths) { - mock(modulePath, mockDefinition) - } - - /** - * Re-requires the specified modules, in case they were required before the mock was set up - * and thus don't have the mocked module - */ - const reRequireDependencies = () => { - for (const dependencyPath of dependencyPaths) { - mock.reRequire(dependencyPath) - } - } - reRequireDependencies() - - const core = { - /** - * Set (or unset) a mocked implementation of a function - */ - mockFunction( - functionName: F, - implementation: MockedFunc - ) { - if (implementation) { - functionReplacements[functionName] = implementation - } else { - delete functionReplacements[functionName] - } - }, - /** - * Remove all mocked function implementations - */ - resetMockedFunctions() { - functionReplacements = {} - }, - /** - * Remove a single function mock - */ - resetMockedFunction(functionName: MockTypeFunctionProp) { - delete functionReplacements[functionName] - }, - /** - * Temporarily disable the mock, sending all function calls to the real implementations - */ - disable() { - isDisabled = true - }, - /** - * Re-enable the mock, if it's been disabled before - */ - enable() { - isDisabled = false - }, - /** - * Unmock entirely - * Note: All requires done before this point will still point to the mocks - */ - destroy(reRequireDeps = true) { - if (preventDestroy) { - isDisabled = true - return - } - - for (const modulePath of modulePaths) { - mock.stop(modulePath) - } - - if (reRequireDeps) reRequireDependencies() - }, - /** - * Re-require specified dependencies - */ - reRequireDependencies - } - - const helpers = { - /** - * Mock a function temporarily - * - * Set 'times' parameter to control how many times will the function be invoked - * with the mocked implementation - * - * Use args & results arrays in result object to see the passed in arguments and function return values - * that were collected - */ - hijackFunction( - functionName: F, - implementation: MockedFunc, - params: { times: number } = { times: 1 } - ) { - let { times } = params - if (!isFunction(implementation)) - throw new Error('Implementation must be a function') - - const collectedReturns: Array>> = [] - const collectedArgs: Array>> = [] - - core.enable() - core.mockFunction( - functionName, - function (this: unknown, ...args: Parameters>) { - const returnVal = implementation.apply(this, args) - times-- - - if (times <= 0) { - core.resetMockedFunction(functionName) - } - - collectedArgs.push(args) - collectedReturns.push(returnVal) - - return returnVal - } - ) - - return { - /** - * Arguments that were used to call the mocked function. Each entry in this array is an array of arguments, so use the first array dimension to choose - * the invocation and the 2nd dimension to choose the specific argument. - */ - args: collectedArgs, - /** - * Return values that were returned from the mocked function. - */ - returns: collectedReturns, - /** - * Get the amount of invocations - */ - length: () => collectedArgs.length - } - }, - /** - * Simplification of hijackFunction for factories - */ - hijackFactoryFunction( - functionName: F, - implementation: FactoryResultOf, - params: { times: number } = { times: 1 } - ) { - const { times } = params - if (!isFunction(implementation)) - throw new Error('Implementation must be a function') - - const collectedReturns: Array< - ReturnType> - > = [] - const collectedArgs: Array< - Parameters> - > = [] - - core.enable() - core.mockFunction(functionName, (() => { - let localTimes = times - - return (...args: Parameters>) => { - // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type - const returnVal = (implementation as Function).apply({}, args) - localTimes-- - - if (localTimes <= 0) { - core.resetMockedFunction(functionName) - } - - collectedArgs.push(args) - collectedReturns.push(returnVal) - - return returnVal - } - }) as ReturnType) - - return { - /** - * Arguments that were used to call the mocked function. Each entry in this array is an array of arguments, so use the first array dimension to choose - * the invocation and the 2nd dimension to choose the specific argument. - */ - args: collectedArgs, - /** - * Return values that were returned from the mocked function. - */ - returns: collectedReturns, - /** - * Get the amount of invocations - */ - length: () => collectedArgs.length - } - } - } - - return { - ...core, - ...helpers - } -} - -export type MockApiType = ReturnType - -/** - * Create global mock. Essentially the same as mockRequireModule() but simplified - * with safeguards so that you can't destroy it and break it in other tests - * - * Note: Global mocks should be registered in test/hooks.js before everything else! - */ -export function createGlobalMock>( - modulePath: string -) { - const globalMock = mockRequireModule([modulePath], [], { - preventDestroy: true - }) - const { hijackFunction, resetMockedFunctions } = globalMock - - return { - hijackFunction, - resetMockedFunctions - } -} diff --git a/packages/server/test/mocks/global.ts b/packages/server/test/mocks/global.ts index 5f32df19c..a08832bfd 100644 --- a/packages/server/test/mocks/global.ts +++ b/packages/server/test/mocks/global.ts @@ -1,54 +1,14 @@ -import { createGlobalMock, mockRequireModule } from '@/test/mockHelper' - -/** - * Global mocks that can be re-used. Early setup ensures that mocks work. - */ - -export const EmailSendingServiceMock = createGlobalMock< - typeof import('@/modules/emails/services/sending') ->('@/modules/emails/services/sending') - -export const CommentsRepositoryMock = mockRequireModule< - typeof import('@/modules/comments/repositories/comments') ->(['@/modules/comments/repositories/comments']) - -export const MultiRegionDbSelectorMock = mockRequireModule< - typeof import('@/modules/multiregion/utils/dbSelector') ->(['@/modules/multiregion/utils/dbSelector']) - -export const MultiRegionBlobStorageSelectorMock = mockRequireModule< - typeof import('@/modules/multiregion/utils/blobStorageSelector') ->(['@/modules/multiregion/utils/blobStorageSelector']) - -export const MultiRegionConfigMock = mockRequireModule< - typeof import('@/modules/multiregion/regionConfig') ->(['@/modules/multiregion/regionConfig']) - -export const StripeClientMock = mockRequireModule< - typeof import('@/modules/gatekeeper/clients/stripe') ->(['@/modules/gatekeeper/clients/stripe']) - -export const EnvHelperMock = mockRequireModule< - typeof import('@/modules/shared/helpers/envHelper') ->( - [ - '@/modules/shared/helpers/envHelper', - require.resolve('../../modules/shared/helpers/envHelper') - ], - ['@/modules/shared/index'] -) - -export const StreamsRepositoryMock = mockRequireModule< - typeof import('@/modules/core/repositories/streams') ->(['@/modules/core/repositories/streams']) +import { adminOverrideEnabled } from '@/modules/shared/helpers/envHelper' export const mockAdminOverride = () => { + const baseValue = adminOverrideEnabled() + const enable = (enabled: boolean) => { - EnvHelperMock.mockFunction('adminOverrideEnabled', () => enabled) + process.env.ADMIN_OVERRIDE_ENABLED = enabled.toString() } const disable = () => { - EnvHelperMock.resetMockedFunction('adminOverrideEnabled') + process.env.ADMIN_OVERRIDE_ENABLED = baseValue.toString() } return { enable, disable } diff --git a/packages/server/test/notificationsHelper.ts b/packages/server/test/notificationsHelper.ts index 559ec7979..ffcff590e 100644 --- a/packages/server/test/notificationsHelper.ts +++ b/packages/server/test/notificationsHelper.ts @@ -2,8 +2,11 @@ import { notificationsLogger as logger } from '@/observability/logging' import { getQueue, NotificationJobResult } from '@/modules/notifications/services/queue' import { EventEmitter } from 'events' import { CompletedEventCallback, FailedEventCallback, JobId } from 'bull' -import { pick } from 'lodash' +import { pick } from 'lodash-es' import { Nullable } from '@speckle/shared' +import { getEventBus } from '@/modules/shared/services/eventBus' +import { NotificationsEvents } from '@/modules/notifications/domain/events' +import { NotificationMessage } from '@/modules/notifications/helpers/types' type AckEvent = { result?: NotificationJobResult @@ -15,6 +18,7 @@ const NEW_ACK_EVENT = 'new-ack' export function buildNotificationsStateTracker() { const queue = getQueue() + const eventBus = getEventBus() const localEvents = new EventEmitter() const ackHandler = (e: AckEvent) => { @@ -35,6 +39,15 @@ export function buildNotificationsStateTracker() { queue.on('failed', failedHandler) const collectedAcks = new Map() + const collectedMessages: NotificationMessage[] = [] + + // Listen for incoming messages + const quitEventBusListen = eventBus.listen( + NotificationsEvents.Received, + async ({ payload }) => { + collectedMessages.push(payload.message) + } + ) return { /** @@ -44,6 +57,7 @@ export function buildNotificationsStateTracker() { queue.removeListener('completed', completedHandler) queue.removeListener('failed', failedHandler) localEvents.removeAllListeners() + quitEventBusListen() }, /** @@ -51,6 +65,7 @@ export function buildNotificationsStateTracker() { */ reset: () => { collectedAcks.clear() + collectedMessages.length = 0 }, /** @@ -128,7 +143,9 @@ export function buildNotificationsStateTracker() { localEvents.off(NEW_ACK_EVENT, ackTracker) localEvents.off(NEW_ACK_EVENT, promiseAckTracker) }) - } + }, + + collectedMessages: () => collectedMessages.slice() } } diff --git a/packages/server/test/plugins/graphql.ts b/packages/server/test/plugins/graphql.ts index d78927071..d568a0e6d 100644 --- a/packages/server/test/plugins/graphql.ts +++ b/packages/server/test/plugins/graphql.ts @@ -1,7 +1,7 @@ import { Optional } from '@/modules/shared/helpers/typeHelper' import { ExecuteOperationResponse } from '@/test/graphqlHelper' import { AssertionError } from 'chai' -import { isString } from 'lodash' +import { isString } from 'lodash-es' type ChaiPluginThis> = { __flags: { diff --git a/packages/server/test/speckle-helpers/branchHelper.ts b/packages/server/test/speckle-helpers/branchHelper.ts index 35a673d78..51b0bb2b6 100644 --- a/packages/server/test/speckle-helpers/branchHelper.ts +++ b/packages/server/test/speckle-helpers/branchHelper.ts @@ -7,7 +7,7 @@ import { getProjectDbClient } from '@/modules/multiregion/utils/dbSelector' import { getEventBus } from '@/modules/shared/services/eventBus' import { BasicTestUser } from '@/test/authHelper' import { BasicTestStream } from '@/test/speckle-helpers/streamHelper' -import { omit } from 'lodash' +import { omit } from 'lodash-es' export type BasicTestBranch = { name: string diff --git a/packages/server/test/speckle-helpers/email.ts b/packages/server/test/speckle-helpers/email.ts new file mode 100644 index 000000000..4f4863fb4 --- /dev/null +++ b/packages/server/test/speckle-helpers/email.ts @@ -0,0 +1,103 @@ +import { EmailsEvents } from '@/modules/emails/domain/events' +import { getEventBus } from '@/modules/shared/services/eventBus' +import { MaybeAsync } from '@speckle/shared' +import type Mail from 'nodemailer/lib/mailer' + +type ListenOptions = { + handler?: (email: Mail.Options) => MaybeAsync + times?: number +} + +export const createEmailListener = async ( + options?: Partial<{ + destroyWhenNoListeners: boolean + }> +) => { + const eventBus = getEventBus() + let collectedSends: Mail.Options[] = [] + let listenerQuitters: (() => void)[] = [] + + // Global listener, tracks emails even if no listen() invoked + const quitGlobal = eventBus.listen( + EmailsEvents.PreparingToSend, + async ({ payload }) => { + collectedSends.push(payload.options) + } + ) + + /** + * Reset .listen() calls and collected sends (by default) + */ + const reset = ( + options?: Partial<{ + listenersOnly: boolean + }> + ) => { + listenerQuitters.forEach((quit) => quit()) + listenerQuitters = [] + + if (!options?.listenersOnly) { + collectedSends = [] + } + } + + /** + * Close all listeners + */ + const destroy = async () => { + quitGlobal() + reset({ listenersOnly: true }) + } + + /** + * Start a listening session w/ localized collected sends + */ + const listen = (params: ListenOptions) => { + let timesReceived = 0 + const localSends: Mail.Options[] = [] + + const quit = eventBus.listen(EmailsEvents.PreparingToSend, async ({ payload }) => { + await params.handler?.(payload.options) + localSends.push(payload.options) + timesReceived += 1 + + if (params.times && timesReceived >= params.times) { + await wrappedQuit() + } + }) + + const wrappedQuit = async () => { + quit() + listenerQuitters.splice(listenerQuitters.indexOf(wrappedQuit), 1) + + // Destroy all listeners, if last one + if (options?.destroyWhenNoListeners && listenerQuitters.length === 0) { + await destroy() + } + } + + listenerQuitters.push(wrappedQuit) + + return { + /** + * Get sends collected during this listening session. + */ + getSends: () => localSends.slice(), + quit: wrappedQuit + } + } + + /** + * Get all sends collected (even outside of listener sessions) + */ + const getSends = () => collectedSends.slice() + + return { + destroy, + listen, + getSends, + reset + } +} + +export type TestEmailListener = Awaited> diff --git a/packages/server/test/speckle-helpers/error.ts b/packages/server/test/speckle-helpers/error.ts index 1778f2a03..235a660e7 100644 --- a/packages/server/test/speckle-helpers/error.ts +++ b/packages/server/test/speckle-helpers/error.ts @@ -1,5 +1,5 @@ import { BaseError } from '@/modules/shared/errors' -import { VError } from 'verror' +import VError from 'verror' /** * Generic VError-enhanced error for usage in tests diff --git a/packages/server/test/speckle-helpers/inviteHelper.ts b/packages/server/test/speckle-helpers/inviteHelper.ts index 815e98d9a..be2ecd212 100644 --- a/packages/server/test/speckle-helpers/inviteHelper.ts +++ b/packages/server/test/speckle-helpers/inviteHelper.ts @@ -28,7 +28,6 @@ import { ServerInviteRecord, ServerInviteResourceTarget } from '@/modules/serverinvites/domain/types' -import { EmailSendingServiceMock } from '@/test/mocks/global' import { getStreamFactory, getStreamRolesFactory, @@ -58,6 +57,8 @@ import { validateAndCreateUserEmailFactory } from '@/modules/core/services/userE import { requestNewEmailVerificationFactory } from '@/modules/emails/services/verification/request' import { deleteOldAndInsertNewVerificationFactory } from '@/modules/emails/repositories' import { renderEmail } from '@/modules/emails/services/emailRendering' +import { createEmailListener } from '@/test/speckle-helpers/email' +import type Mail from 'nodemailer/lib/mailer' const getServerInfo = getServerInfoFactory({ db }) const getUser = getUserFactory({ db }) @@ -199,13 +200,15 @@ export const createStreamInviteDirectly = async ( ) } -function getInviteTokenFromEmailParams(emailParams: SendEmailParams) { +function getInviteTokenFromEmailParams(emailParams: SendEmailParams | Mail.Options) { const { text } = emailParams - const [, inviteId] = text.match(/\?token=(.*?)(\s|&)/i) || [] + const [, inviteId] = (text?.toString() || '').match(/\?token=(.*?)(\s|&)/i) || [] return inviteId } -export async function validateInviteExistanceFromEmail(emailParams: SendEmailParams) { +export async function validateInviteExistanceFromEmail( + emailParams: SendEmailParams | Mail.Options +) { const findInviteByToken = findInviteByTokenFactory({ db }) // Validate that invite exists @@ -222,15 +225,14 @@ export async function validateInviteExistanceFromEmail(emailParams: SendEmailPar * created through whatever logic is passed in the createInvite function */ export const captureCreatedInvite = async (createInvite: () => MaybeAsync) => { - const sendEmailInvocations = EmailSendingServiceMock.hijackFunction( - 'sendEmail', - async () => true - ) + const emailListener = await createEmailListener({ destroyWhenNoListeners: true }) + const { getSends } = emailListener.listen({ times: 1 }) await Promise.resolve(createInvite()) - expect(sendEmailInvocations.args).to.have.lengthOf(1) - const emailParams = sendEmailInvocations.args[0][0] + const emails = getSends() + expect(emails).to.have.lengthOf(1) + const emailParams = emails[0] expect(emailParams).to.be.ok return await validateInviteExistanceFromEmail(emailParams) diff --git a/packages/server/test/speckle-helpers/regions.ts b/packages/server/test/speckle-helpers/regions.ts index c293d17f3..aedf4c210 100644 --- a/packages/server/test/speckle-helpers/regions.ts +++ b/packages/server/test/speckle-helpers/regions.ts @@ -13,7 +13,7 @@ import { getMainTestRegionKey } from '@/test/hooks' import { wait } from '@speckle/shared' -import { isString } from 'lodash' +import { isString } from 'lodash-es' /** * Delete all regions entries that are not part of the main multi region mode diff --git a/packages/server/test/speckle-helpers/streamHelper.ts b/packages/server/test/speckle-helpers/streamHelper.ts index fe4d10533..3bffa1b60 100644 --- a/packages/server/test/speckle-helpers/streamHelper.ts +++ b/packages/server/test/speckle-helpers/streamHelper.ts @@ -62,7 +62,7 @@ import { BasicTestUser } from '@/test/authHelper' import { ProjectVisibility } from '@/test/graphql/generated/graphql' import { faker } from '@faker-js/faker' import { ensureError, Roles, StreamRoles } from '@speckle/shared' -import { omit } from 'lodash' +import { omit } from 'lodash-es' const getServerInfo = getServerInfoFactory({ db }) const getUsers = getUsersFactory({ db }) diff --git a/packages/server/tsconfig.build.json b/packages/server/tsconfig.build.json index e8a26d18b..1f25dd062 100644 --- a/packages/server/tsconfig.build.json +++ b/packages/server/tsconfig.build.json @@ -1,4 +1,11 @@ { "extends": "./tsconfig.json", - "exclude": ["**/*.spec.js", "**/*.spec.ts", "./modules/cli/**/*", "test/**/*"] + "exclude": [ + "**/*.spec.js", + "**/*.spec.ts", + "modules/cli/**/*", + "**/tests/**/*", + "test/**/*", + "codegen.ts" + ] } diff --git a/packages/server/tsconfig.json b/packages/server/tsconfig.json index 545d903f4..025f58aad 100644 --- a/packages/server/tsconfig.json +++ b/packages/server/tsconfig.json @@ -23,9 +23,9 @@ // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ /* Modules */ - "module": "node16" /* Specify what module code is generated. */, + "module": "preserve" /* Specify what module code is generated. */, // "rootDir": "./", /* Specify the root folder within your source files. */ - "moduleResolution": "node16" /* Specify how TypeScript looks up a file from a given module specifier. */, + "moduleResolution": "bundler" /* Specify how TypeScript looks up a file from a given module specifier. */, "baseUrl": "./" /* Specify the base directory to resolve non-relative module names. */, "paths": { "@/*": ["./*"], @@ -108,13 +108,16 @@ "db/**/*", "healthchecks/**/*", "modules/**/*", + "modules/**/*.cts", + "modules/schema.cts", "observability/**/*", "scripts/**/*", "test/**/*", "type-augmentations/**/*", "app.ts", "bootstrap.js", - "knexfile.ts" + "knexfile.ts", + "codegen.ts" ], "exclude": ["node_modules", "coverage", "reports"] } diff --git a/packages/shared/package.json b/packages/shared/package.json index c7a29b8cf..724150e6e 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -88,6 +88,7 @@ "./pinoPrettyTransport.cjs": "./pinoPrettyTransport.cjs", "./environment": "./src/environment/index.ts", "./environment/db": "./src/environment/db.ts", + "./environment/node": "./src/environment/node.ts", "./environment/multiRegionConfig": "./src/environment/db.ts", "./observability": "./src/observability/index.ts", "./observability/mixpanel": "./src/observability/mixpanel.ts", @@ -101,6 +102,8 @@ "./workers/fileimport": "./src/workers/fileimport/index.ts", "./viewer": "./src/viewer/index.ts", "./viewer/route": "./src/viewer/helpers/route.ts", + "./viewer/state": "./src/viewer/helpers/state.ts", + "./automate": "./src/automate/index.ts", "./dist/*": "./dist/*" }, "exclude": [ @@ -147,6 +150,16 @@ "default": "./dist/commonjs/environment/db.js" } }, + "./environment/node": { + "import": { + "types": "./dist/esm/environment/node.d.ts", + "default": "./dist/esm/environment/node.js" + }, + "require": { + "types": "./dist/commonjs/environment/node.d.ts", + "default": "./dist/commonjs/environment/node.js" + } + }, "./environment/multiRegionConfig": { "import": { "types": "./dist/esm/environment/db.d.ts", @@ -277,6 +290,26 @@ "default": "./dist/commonjs/viewer/helpers/route.js" } }, + "./viewer/state": { + "import": { + "types": "./dist/esm/viewer/helpers/state.d.ts", + "default": "./dist/esm/viewer/helpers/state.js" + }, + "require": { + "types": "./dist/commonjs/viewer/helpers/state.d.ts", + "default": "./dist/commonjs/viewer/helpers/state.js" + } + }, + "./automate": { + "import": { + "types": "./dist/esm/automate/index.d.ts", + "default": "./dist/esm/automate/index.js" + }, + "require": { + "types": "./dist/commonjs/automate/index.d.ts", + "default": "./dist/commonjs/automate/index.js" + } + }, "./dist/*": "./dist/*" } } diff --git a/packages/shared/src/authz/index.ts b/packages/shared/src/authz/index.ts index ed7c4aa17..f68f8de73 100644 --- a/packages/shared/src/authz/index.ts +++ b/packages/shared/src/authz/index.ts @@ -8,3 +8,4 @@ export * from './helpers/graphql.js' export * from './domain/authErrors.js' export { AuthPolicyResult } from './domain/policies.js' export { PersonalProjectsLimits } from './domain/projects/limits.js' +export * from './domain/workspaces/operations.js' diff --git a/packages/shared/src/environment/db.spec.ts b/packages/shared/src/environment/db.spec.ts new file mode 100644 index 000000000..1fb51cdc3 --- /dev/null +++ b/packages/shared/src/environment/db.spec.ts @@ -0,0 +1,48 @@ +import { afterEach, beforeAll, describe, expect, it } from 'vitest' +import { regionConfigSchema } from './db.js' + +describe('Database Configuration', () => { + let baseNodeEnv: string + + beforeAll(() => { + baseNodeEnv = process.env.NODE_ENV || '' + }) + + afterEach(() => { + process.env.NODE_ENV = baseNodeEnv + }) + + it('regionConfigSchema does not allow skipInitialization in non-test environments', () => { + const validConfig = { + postgres: { + connectionUri: 'postgres://user:password@host:port/dbname', + databaseName: 'dbname', + privateConnectionUri: 'postgres://user:password@host:port/dbname', + publicTlsCertificate: 'cert', + skipInitialization: false + }, + blobStorage: { + endpoint: 'https://s3.example.com', + publicEndpoint: 'https://public.s3.example.com', + accessKey: 'accessKey', + secretKey: 'secretKey', + bucket: 'bucketName', + createBucketIfNotExists: true, + s3Region: 'us-west-1' + } + } + + process.env.NODE_ENV = 'test' // this should work + expect(() => regionConfigSchema.parse(validConfig)).not.toThrow() + + const invalidConfig = { + ...validConfig, + postgres: { ...validConfig.postgres, skipInitialization: true } + } + + process.env.NODE_ENV = 'production' // this should throw + expect(() => regionConfigSchema.parse(invalidConfig)).toThrow( + /skipInitialization can only be set when NODE_ENV is \\\"test\\\"/ + ) + }) +}) diff --git a/packages/shared/src/environment/db.ts b/packages/shared/src/environment/db.ts index 9c806ee47..57e673539 100644 --- a/packages/shared/src/environment/db.ts +++ b/packages/shared/src/environment/db.ts @@ -7,7 +7,7 @@ import { isUndefined, get } from '#lodash' // cause of knex's ESM/CJS interop issues const knex = get(Knex, 'knex') || get(Knex, 'default') -const regionConfigSchema = z.object({ +export const regionConfigSchema = z.object({ postgres: z.object({ connectionUri: z .string() @@ -29,7 +29,16 @@ const regionConfigSchema = z.object({ publicTlsCertificate: z .string() .describe('Public TLS ("CA") certificate for the Postgres server') + .optional(), + skipInitialization: z + .boolean() .optional() + .describe( + 'Skip database initialization (migration run & replication setup). Only used in tests.' + ) + .refine((val) => val !== true || process.env.NODE_ENV === 'test', { + message: 'skipInitialization can only be set when NODE_ENV is "test"' + }) }), blobStorage: z.object({ endpoint: z diff --git a/packages/shared/src/environment/node.ts b/packages/shared/src/environment/node.ts new file mode 100644 index 000000000..ecf85ff88 --- /dev/null +++ b/packages/shared/src/environment/node.ts @@ -0,0 +1,11 @@ +import { fileURLToPath } from 'url' +import { dirname } from 'path' + +/** + * Feed in import.meta and get the module's filesystem location + */ +export const getModuleDirectory = (meta: ImportMeta): string => { + const __filename = fileURLToPath(meta.url) + const __dirname = dirname(__filename) + return __dirname +} diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index 2bb252c23..10cbb5431 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -1,3 +1,4 @@ +/** TODO: We should get rid of the index barrel file at some point entirely and exclusively rely on exports maps */ export * as RichTextEditor from './rich-text-editor/index.js' export * as SpeckleViewer from './viewer/index.js' export * as Automate from './automate/index.js' diff --git a/packages/shared/src/observability/index.ts b/packages/shared/src/observability/index.ts index e9fff3704..d11ddc975 100644 --- a/packages/shared/src/observability/index.ts +++ b/packages/shared/src/observability/index.ts @@ -12,6 +12,9 @@ type LogFormatter = (logObject: Record) => Record !!s?.length) const defaultLevelFormatterFactory = (pretty: boolean): LogLevelFormatter => @@ -42,7 +45,23 @@ export function getLogger( messageKey: pretty ? 'msg' : '@mt', level: minimumLoggedLevel, // when not pretty, we need the time in the clef appropriate field, not from pino - timestamp: pretty ? pino.stdTimeFunctions.isoTime : false + timestamp: pretty ? pino.stdTimeFunctions.isoTime : false, + hooks: { + logMethod(args, method) { + // Invoke as is + if (!debugNamespaces.length) { + return method.apply(this, args) + } + + // Filter out if component not in allowed debug namespaces + const component = (this.bindings() as { component: string }).component + if (debugNamespaces.includes(component)) { + return method.apply(this, args) + } + + // Otherwise, skip actually logging + } + } } // pino-pretty hangs in debugger mode in node 22 for some (Ubuntu/WSL2?), dunno why @@ -52,7 +71,7 @@ export function getLogger( options: { colorize: true, destination: 2, //stderr - ignore: 'time', + // ignore: 'time', levelFirst: true, singleLine: true } @@ -60,6 +79,7 @@ export function getLogger( } logger = pino(pinoOptions) + return logger } diff --git a/vitest.workspace.mjs b/vitest.workspace.mjs index 526d09ded..2f87b98e5 100644 --- a/vitest.workspace.mjs +++ b/vitest.workspace.mjs @@ -1 +1 @@ -export default ['packages/*/vitest.config.{js,ts,mjs}'] +export default ['packages/*/vitest.config.{mjs,cjs,js,ts,mjs}'] diff --git a/yarn.lock b/yarn.lock index c5d4a7667..31c358218 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10701,6 +10701,60 @@ __metadata: languageName: node linkType: hard +"@graphql-codegen/cli@npm:^5.0.7": + version: 5.0.7 + resolution: "@graphql-codegen/cli@npm:5.0.7" + 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.8.2" + "@graphql-codegen/core": "npm:^4.0.2" + "@graphql-codegen/plugin-helpers": "npm:^5.1.1" + "@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.1.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.10.0" + 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/89be5d1ad02efae0ff23b82f045b8c41445a7e78c3a0bb130ee762bfcae6915a5438d7c4fcd2a11c7529877bba872ed93a19398fc73f15d7877ede5c23faba87 + languageName: node + linkType: hard + "@graphql-codegen/client-preset@npm:^4.6.0, @graphql-codegen/client-preset@npm:^4.6.4": version: 4.6.4 resolution: "@graphql-codegen/client-preset@npm:4.6.4" @@ -10724,6 +10778,33 @@ __metadata: languageName: node linkType: hard +"@graphql-codegen/client-preset@npm:^4.8.2": + version: 4.8.3 + resolution: "@graphql-codegen/client-preset@npm:4.8.3" + 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.17" + "@graphql-codegen/plugin-helpers": "npm:^5.1.1" + "@graphql-codegen/typed-document-node": "npm:^5.1.2" + "@graphql-codegen/typescript": "npm:^4.1.6" + "@graphql-codegen/typescript-operations": "npm:^4.6.1" + "@graphql-codegen/visitor-plugin-common": "npm:^5.8.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 + graphql-sock: ^1.0.0 + peerDependenciesMeta: + graphql-sock: + optional: true + checksum: 10/8204ab84f00a67501c8fc0eb3178dcd980f88aa33c5a72a89dc7d831f8a93c72c116ee90ac286bdcf7f02a3099eed3320383d77eca8d9108f618bac09df0e713 + languageName: node + linkType: hard + "@graphql-codegen/core@npm:^4.0.2": version: 4.0.2 resolution: "@graphql-codegen/core@npm:4.0.2" @@ -10753,6 +10834,21 @@ __metadata: languageName: node linkType: hard +"@graphql-codegen/gql-tag-operations@npm:4.0.17": + version: 4.0.17 + resolution: "@graphql-codegen/gql-tag-operations@npm:4.0.17" + dependencies: + "@graphql-codegen/plugin-helpers": "npm:^5.1.0" + "@graphql-codegen/visitor-plugin-common": "npm:5.8.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/09c51ffa642eb3a0b6bee94dd40214ebecabbd1821a70bbfb9798032486abc05487067cd1e67fa75331b31c3acb546ce8c32081debb630d509b05738d03dd5e7 + languageName: node + linkType: hard + "@graphql-codegen/plugin-helpers@npm:^5.0.3": version: 5.0.4 resolution: "@graphql-codegen/plugin-helpers@npm:5.0.4" @@ -10785,6 +10881,22 @@ __metadata: languageName: node linkType: hard +"@graphql-codegen/plugin-helpers@npm:^5.1.1": + version: 5.1.1 + resolution: "@graphql-codegen/plugin-helpers@npm:5.1.1" + 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/bfcdaab56a4bbf7e80dc4f8040c76a6b081ee748ecce8570bcb04e3b78fa4f07d5dede2545c228d628c567b8f19e24077d3b374ae79fe927205d4365ef730a5c + 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" @@ -10813,9 +10925,9 @@ __metadata: languageName: node linkType: hard -"@graphql-codegen/typed-document-node@npm:^5.1.1": - version: 5.1.1 - resolution: "@graphql-codegen/typed-document-node@npm:5.1.1" +"@graphql-codegen/typed-document-node@npm:^5.1.2": + version: 5.1.2 + resolution: "@graphql-codegen/typed-document-node@npm:5.1.2" dependencies: "@graphql-codegen/plugin-helpers": "npm:^5.1.0" "@graphql-codegen/visitor-plugin-common": "npm:5.8.0" @@ -10824,7 +10936,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/213983a6c10173dc61041f331c19d6152a7e1bb07ab10c70355d49a7048a367ccee30428eda652b514c32431f938606b1db5c04f0d2c9dd93abb0096187cb91a + checksum: 10/9c24ed2f4f76e616d335386cf78401b52b1766705dc972513066a5145f06781502e758cb3d138cbe7faae2b9f9e108cf3f8b74bf7936d838b59314a1e29ee1eb languageName: node linkType: hard @@ -10843,9 +10955,9 @@ __metadata: languageName: node linkType: hard -"@graphql-codegen/typescript-operations@npm:^4.6.0": - version: 4.6.0 - resolution: "@graphql-codegen/typescript-operations@npm:4.6.0" +"@graphql-codegen/typescript-operations@npm:^4.6.1": + version: 4.6.1 + resolution: "@graphql-codegen/typescript-operations@npm:4.6.1" dependencies: "@graphql-codegen/plugin-helpers": "npm:^5.1.0" "@graphql-codegen/typescript": "npm:^4.1.6" @@ -10855,13 +10967,16 @@ __metadata: 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 graphql-sock: ^1.0.0 - checksum: 10/6b835dce1db8f73f9f1d51ff8258f1cccbd40618a3582c923eb9ee1761a481502beaea719869d96741e5dd3a4cc2bb87c4fd0f6ab369f7dc22fbc838f6e7751f + peerDependenciesMeta: + graphql-sock: + optional: true + checksum: 10/4161ef907340ec1112801545fdb99ee5b3ee411d77f990c167acf81235d13dc58f07015de06a8e12030ef609b4f2e795521e3c9373df725715ed04b61b5a2c84 languageName: node linkType: hard -"@graphql-codegen/typescript-resolvers@npm:^4.5.0": - version: 4.5.0 - resolution: "@graphql-codegen/typescript-resolvers@npm:4.5.0" +"@graphql-codegen/typescript-resolvers@npm:^4.5.1": + version: 4.5.1 + resolution: "@graphql-codegen/typescript-resolvers@npm:4.5.1" dependencies: "@graphql-codegen/plugin-helpers": "npm:^5.1.0" "@graphql-codegen/typescript": "npm:^4.1.6" @@ -10872,7 +10987,10 @@ __metadata: 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 graphql-sock: ^1.0.0 - checksum: 10/02986bacf81da793a3501c5bbf810a0eb53b56cf42fbb760f62e8d1a9a6d929a35dff22d01bc1120a075f23a6ded35a6fd14d0c4cbb3d49effe7b4ecb9234ad4 + peerDependenciesMeta: + graphql-sock: + optional: true + checksum: 10/0159ae5362bfd45690fac4104153bedf1243494159f7ea9cc72c23e80f0191c889aa24bb34ef7810f1de600d668687ad5b247b5c2c3d419c001379cf0e3fc4ce languageName: node linkType: hard @@ -10926,7 +11044,7 @@ __metadata: languageName: node linkType: hard -"@graphql-codegen/visitor-plugin-common@npm:5.8.0": +"@graphql-codegen/visitor-plugin-common@npm:5.8.0, @graphql-codegen/visitor-plugin-common@npm:^5.8.0": version: 5.8.0 resolution: "@graphql-codegen/visitor-plugin-common@npm:5.8.0" dependencies: @@ -11186,6 +11304,20 @@ __metadata: languageName: node linkType: hard +"@graphql-tools/load@npm:^8.1.0": + version: 8.1.0 + resolution: "@graphql-tools/load@npm:8.1.0" + dependencies: + "@graphql-tools/schema": "npm:^10.0.23" + "@graphql-tools/utils": "npm:^10.8.6" + p-limit: "npm:3.1.0" + tslib: "npm:^2.4.0" + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + checksum: 10/4601dda7eb32cb8afed2379102ad82f8a948e478f42c7b1f354a3468ca8dfcdcc2a89e6c6ebcbb574c77eaa80d47f20c27230bdcd6c2d0a3600fa1d6a450cc95 + languageName: node + linkType: hard + "@graphql-tools/merge@npm:^8.4.1": version: 8.4.2 resolution: "@graphql-tools/merge@npm:8.4.2" @@ -11210,6 +11342,18 @@ __metadata: languageName: node linkType: hard +"@graphql-tools/merge@npm:^9.0.24": + version: 9.0.24 + resolution: "@graphql-tools/merge@npm:9.0.24" + dependencies: + "@graphql-tools/utils": "npm:^10.8.6" + tslib: "npm:^2.4.0" + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + checksum: 10/95f77ff141f10d5d726cd8d1ae1ad84ed944c84346bf20461adca9b1543bb94cb524b0347885fe61d3158ccf5ffe1dddec361787ae40bfcc3449aad51528dd77 + languageName: node + linkType: hard + "@graphql-tools/merge@npm:^9.0.6": version: 9.0.7 resolution: "@graphql-tools/merge@npm:9.0.7" @@ -11300,6 +11444,19 @@ __metadata: languageName: node linkType: hard +"@graphql-tools/schema@npm:^10.0.23": + version: 10.0.23 + resolution: "@graphql-tools/schema@npm:10.0.23" + dependencies: + "@graphql-tools/merge": "npm:^9.0.24" + "@graphql-tools/utils": "npm:^10.8.6" + tslib: "npm:^2.4.0" + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + checksum: 10/f0960dae161a478941276df1802af09844825c8135e4695b36f5f7e7384f43ff8e1288a67546023fc861951d783327f239112ccf563cb4be1f22038fc78acf21 + languageName: node + linkType: hard + "@graphql-tools/schema@npm:^10.0.6": version: 10.0.6 resolution: "@graphql-tools/schema@npm:10.0.6" @@ -11393,6 +11550,21 @@ __metadata: languageName: node linkType: hard +"@graphql-tools/utils@npm:^10.8.6": + version: 10.8.6 + resolution: "@graphql-tools/utils@npm:10.8.6" + dependencies: + "@graphql-typed-document-node/core": "npm:^3.1.1" + "@whatwg-node/promise-helpers": "npm:^1.0.0" + cross-inspect: "npm:1.0.1" + dset: "npm:^3.1.4" + tslib: "npm:^2.4.0" + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + checksum: 10/98329aef966b489d3674eb086b784f6fb4500afaf9bc46fbe6a14ca32e98fec480c7395d3488c5eb2f450b75a538e98edf0527ed4bf24af352230e850c914389 + languageName: node + linkType: hard + "@graphql-tools/utils@npm:^9.2.1": version: 9.2.1 resolution: "@graphql-tools/utils@npm:9.2.1" @@ -16055,11 +16227,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.5" - "@graphql-codegen/typed-document-node": "npm:^5.1.1" + "@graphql-codegen/cli": "npm:^5.0.7" + "@graphql-codegen/typed-document-node": "npm:^5.1.2" "@graphql-codegen/typescript": "npm:^4.1.6" - "@graphql-codegen/typescript-operations": "npm:^4.6.0" - "@graphql-codegen/typescript-resolvers": "npm:^4.5.0" + "@graphql-codegen/typescript-operations": "npm:^4.6.1" + "@graphql-codegen/typescript-resolvers": "npm:^4.5.1" "@graphql-tools/mock": "npm:^9.0.4" "@graphql-tools/schema": "npm:^10.0.6" "@isaacs/ttlcache": "npm:^1.4.1" @@ -16076,7 +16248,6 @@ __metadata: "@parcel/watcher": "npm:^2.5.1" "@speckle/objectloader": "workspace:^" "@speckle/shared": "workspace:^" - "@swc/core": "npm:^1.11.11" "@tiptap/core": "npm:^2.0.0-beta.176" "@types/bcrypt": "npm:^5.0.0" "@types/bull": "npm:^3.15.9" @@ -16087,6 +16258,7 @@ __metadata: "@types/content-disposition": "npm:^0.5.9" "@types/cookie-parser": "npm:^1.4.7" "@types/cors": "npm:^2.8.17" + "@types/deasync": "npm:^0" "@types/debug": "npm:^4.1.7" "@types/deep-equal-in-any-order": "npm:^1.0.1" "@types/ejs": "npm:^3.1.1" @@ -16094,10 +16266,10 @@ __metadata: "@types/ioredis-mock": "npm:^8.2.5" "@types/libsodium-wrappers": "npm:^0" "@types/lodash": "npm:^4.14.180" + "@types/lodash-es": "npm:^4.17.12" "@types/mailchimp__mailchimp_marketing": "npm:^3.0.9" "@types/mjml": "npm:^4.7.0" "@types/mocha": "npm:^10.0.0" - "@types/mock-require": "npm:^2.0.1" "@types/module-alias": "npm:^2.0.1" "@types/netmask": "npm:^2.0.0" "@types/node": "npm:^18.19.38" @@ -16117,7 +16289,7 @@ __metadata: "@types/uuid": "npm:^9.0.0" "@types/verror": "npm:^1.10.6" "@types/xml-escape": "npm:^1.1.3" - "@types/yargs": "npm:^17.0.10" + "@types/yargs": "npm:^17.0.33" "@types/zxcvbn": "npm:^4.4.5" "@typescript-eslint/eslint-plugin": "npm:^5.39.0" "@typescript-eslint/parser": "npm:^5.39.0" @@ -16142,6 +16314,7 @@ __metadata: csv-parse: "npm:^5.6.0" dataloader: "npm:^2.2.3" dayjs: "npm:^1.11.5" + deasync: "npm:^0.1.30" deep-equal-in-any-order: "npm:^1.1.15" dotenv: "npm:^8.2.0" ejs: "npm:^3.1.8" @@ -16153,9 +16326,10 @@ __metadata: express-async-errors: "npm:^3.1.1" express-prom-bundle: "npm:^6.6.0" express-session: "npm:^1.17.1" + extensionless: "npm:^1.9.9" graphql: "npm:^16.6.0" graphql-redis-subscriptions: "npm:^2.2.2" - graphql-scalars: "npm:^1.18.0" + graphql-scalars: "npm:^1.24.1" graphql-subscriptions: "npm:^2.0.0" graphql-tag: "npm:^2.12.6" http-proxy-middleware: "npm:v3.0.0-beta.0" @@ -16166,6 +16340,7 @@ __metadata: knex: "npm:^2.5.1" libsodium-wrappers: "npm:^0.7.13" lodash: "npm:^4.17.21" + lodash-es: "npm:^4.17.21" lru-cache: "npm:^11.0.1" mixpanel: "npm:^0.17.0" mjml: "npm:^4.13.0" @@ -16173,9 +16348,9 @@ __metadata: mocha: "npm:^10.1.0" mocha-junit-reporter: "npm:^2.0.2" mocha-multi: "npm:1.1.7" - mock-require: "npm:^3.0.3" mock-socket: "npm:^9.3.1" module-alias: "npm:^2.2.2" + moq.ts: "npm:10.0.8" netmask: "npm:^2.0.2" node-cron: "npm:^3.0.2" node-machine-id: "npm:^1.1.12" @@ -16207,17 +16382,18 @@ __metadata: subscriptions-transport-ws: "npm:^0.11.0" supertest: "npm:^4.0.2" true-myth: "npm:^8.5.0" - ts-node: "npm:^10.9.2" tsconfig-paths: "npm:^4.0.0" + tsx: "npm:^4.19.4" type-fest: "npm:^4.26.1" typescript: "npm:^4.6.4" typescript-eslint: "npm:^7.12.0" ua-parser-js: "npm:^1.0.38" undici: "npm:^5.28.4" verror: "npm:^1.10.1" + why-is-node-running: "npm:^3.2.2" ws: "npm:^8.17.1" xml-escape: "npm:^1.1.0" - yargs: "npm:^17.3.1" + yargs: "npm:^18.0.0" znv: "npm:^0.4.0" zod: "npm:^3.22.4" zod-express: "npm:^0.0.8" @@ -17506,13 +17682,6 @@ __metadata: languageName: node linkType: hard -"@swc/core-darwin-arm64@npm:1.11.11": - version: 1.11.11 - resolution: "@swc/core-darwin-arm64@npm:1.11.11" - conditions: os=darwin & cpu=arm64 - languageName: node - linkType: hard - "@swc/core-darwin-arm64@npm:1.2.222": version: 1.2.222 resolution: "@swc/core-darwin-arm64@npm:1.2.222" @@ -17534,13 +17703,6 @@ __metadata: languageName: node linkType: hard -"@swc/core-darwin-x64@npm:1.11.11": - version: 1.11.11 - resolution: "@swc/core-darwin-x64@npm:1.11.11" - conditions: os=darwin & cpu=x64 - languageName: node - linkType: hard - "@swc/core-darwin-x64@npm:1.2.222": version: 1.2.222 resolution: "@swc/core-darwin-x64@npm:1.2.222" @@ -17571,13 +17733,6 @@ __metadata: languageName: node linkType: hard -"@swc/core-linux-arm-gnueabihf@npm:1.11.11": - version: 1.11.11 - resolution: "@swc/core-linux-arm-gnueabihf@npm:1.11.11" - conditions: os=linux & cpu=arm - languageName: node - linkType: hard - "@swc/core-linux-arm-gnueabihf@npm:1.2.222": version: 1.2.222 resolution: "@swc/core-linux-arm-gnueabihf@npm:1.2.222" @@ -17601,13 +17756,6 @@ __metadata: languageName: node linkType: hard -"@swc/core-linux-arm64-gnu@npm:1.11.11": - version: 1.11.11 - resolution: "@swc/core-linux-arm64-gnu@npm:1.11.11" - conditions: os=linux & cpu=arm64 & libc=glibc - languageName: node - linkType: hard - "@swc/core-linux-arm64-gnu@npm:1.2.222": version: 1.2.222 resolution: "@swc/core-linux-arm64-gnu@npm:1.2.222" @@ -17629,13 +17777,6 @@ __metadata: languageName: node linkType: hard -"@swc/core-linux-arm64-musl@npm:1.11.11": - version: 1.11.11 - resolution: "@swc/core-linux-arm64-musl@npm:1.11.11" - conditions: os=linux & cpu=arm64 & libc=musl - languageName: node - linkType: hard - "@swc/core-linux-arm64-musl@npm:1.2.222": version: 1.2.222 resolution: "@swc/core-linux-arm64-musl@npm:1.2.222" @@ -17657,13 +17798,6 @@ __metadata: languageName: node linkType: hard -"@swc/core-linux-x64-gnu@npm:1.11.11": - version: 1.11.11 - resolution: "@swc/core-linux-x64-gnu@npm:1.11.11" - conditions: os=linux & cpu=x64 & libc=glibc - languageName: node - linkType: hard - "@swc/core-linux-x64-gnu@npm:1.2.222": version: 1.2.222 resolution: "@swc/core-linux-x64-gnu@npm:1.2.222" @@ -17685,13 +17819,6 @@ __metadata: languageName: node linkType: hard -"@swc/core-linux-x64-musl@npm:1.11.11": - version: 1.11.11 - resolution: "@swc/core-linux-x64-musl@npm:1.11.11" - conditions: os=linux & cpu=x64 & libc=musl - languageName: node - linkType: hard - "@swc/core-linux-x64-musl@npm:1.2.222": version: 1.2.222 resolution: "@swc/core-linux-x64-musl@npm:1.2.222" @@ -17713,13 +17840,6 @@ __metadata: languageName: node linkType: hard -"@swc/core-win32-arm64-msvc@npm:1.11.11": - version: 1.11.11 - resolution: "@swc/core-win32-arm64-msvc@npm:1.11.11" - conditions: os=win32 & cpu=arm64 - languageName: node - linkType: hard - "@swc/core-win32-arm64-msvc@npm:1.2.222": version: 1.2.222 resolution: "@swc/core-win32-arm64-msvc@npm:1.2.222" @@ -17743,13 +17863,6 @@ __metadata: languageName: node linkType: hard -"@swc/core-win32-ia32-msvc@npm:1.11.11": - version: 1.11.11 - resolution: "@swc/core-win32-ia32-msvc@npm:1.11.11" - conditions: os=win32 & cpu=ia32 - languageName: node - linkType: hard - "@swc/core-win32-ia32-msvc@npm:1.2.222": version: 1.2.222 resolution: "@swc/core-win32-ia32-msvc@npm:1.2.222" @@ -17773,13 +17886,6 @@ __metadata: languageName: node linkType: hard -"@swc/core-win32-x64-msvc@npm:1.11.11": - version: 1.11.11 - resolution: "@swc/core-win32-x64-msvc@npm:1.11.11" - conditions: os=win32 & cpu=x64 - languageName: node - linkType: hard - "@swc/core-win32-x64-msvc@npm:1.2.222": version: 1.2.222 resolution: "@swc/core-win32-x64-msvc@npm:1.2.222" @@ -17847,52 +17953,6 @@ __metadata: languageName: node linkType: hard -"@swc/core@npm:^1.11.11": - version: 1.11.11 - resolution: "@swc/core@npm:1.11.11" - dependencies: - "@swc/core-darwin-arm64": "npm:1.11.11" - "@swc/core-darwin-x64": "npm:1.11.11" - "@swc/core-linux-arm-gnueabihf": "npm:1.11.11" - "@swc/core-linux-arm64-gnu": "npm:1.11.11" - "@swc/core-linux-arm64-musl": "npm:1.11.11" - "@swc/core-linux-x64-gnu": "npm:1.11.11" - "@swc/core-linux-x64-musl": "npm:1.11.11" - "@swc/core-win32-arm64-msvc": "npm:1.11.11" - "@swc/core-win32-ia32-msvc": "npm:1.11.11" - "@swc/core-win32-x64-msvc": "npm:1.11.11" - "@swc/counter": "npm:^0.1.3" - "@swc/types": "npm:^0.1.19" - peerDependencies: - "@swc/helpers": "*" - dependenciesMeta: - "@swc/core-darwin-arm64": - optional: true - "@swc/core-darwin-x64": - optional: true - "@swc/core-linux-arm-gnueabihf": - optional: true - "@swc/core-linux-arm64-gnu": - optional: true - "@swc/core-linux-arm64-musl": - optional: true - "@swc/core-linux-x64-gnu": - optional: true - "@swc/core-linux-x64-musl": - optional: true - "@swc/core-win32-arm64-msvc": - optional: true - "@swc/core-win32-ia32-msvc": - optional: true - "@swc/core-win32-x64-msvc": - optional: true - peerDependenciesMeta: - "@swc/helpers": - optional: true - checksum: 10/35c005637c8f3dac8c9718852057f81d39541b848d36a9b46b9aa5300b5d11e3e7911704da2eb025d36b0e334caf646af37ff98a3dee8d4f0fcc5f21e60f551a - languageName: node - linkType: hard - "@swc/core@npm:^1.2.222": version: 1.2.222 resolution: "@swc/core@npm:1.2.222" @@ -18026,15 +18086,6 @@ __metadata: languageName: node linkType: hard -"@swc/types@npm:^0.1.19": - version: 0.1.19 - resolution: "@swc/types@npm:0.1.19" - dependencies: - "@swc/counter": "npm:^0.1.3" - checksum: 10/693147cc9b23147164ddff9cb89477c369fbeb103319584779352a9ff1c72e0a70b97a89dfd97629040db8956d668d7b7a8fed328ffea46a3e8c18577e396994 - languageName: node - linkType: hard - "@swc/wasm@npm:1.2.122": version: 1.2.122 resolution: "@swc/wasm@npm:1.2.122" @@ -18751,6 +18802,13 @@ __metadata: languageName: node linkType: hard +"@types/deasync@npm:^0": + version: 0.1.5 + resolution: "@types/deasync@npm:0.1.5" + checksum: 10/5a8fec89572abc6fbcce7b4f5b5357ba58b2d6d6860b640eb452eb9e87a8d44f787a40450e456b6e2d7fa12cf52bac2ab0d2f9be431dd51bd40584e316ffaa2c + languageName: node + linkType: hard + "@types/debug@npm:^4.1.7": version: 4.1.7 resolution: "@types/debug@npm:4.1.7" @@ -19315,15 +19373,6 @@ __metadata: languageName: node linkType: hard -"@types/mock-require@npm:^2.0.1": - version: 2.0.1 - resolution: "@types/mock-require@npm:2.0.1" - dependencies: - "@types/node": "npm:*" - checksum: 10/8749a4b3fcb9f3d6ebaeff442f00997ca59c4806bc00fea648a1fd06b1ea8510a6900b8e47070561ddf15ce98abc80dfe24ff21a307c2b0d1a6845bd865f708b - languageName: node - linkType: hard - "@types/module-alias@npm:^2.0.1": version: 2.0.1 resolution: "@types/module-alias@npm:2.0.1" @@ -19371,62 +19420,12 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:*, @types/node@npm:>=12": - version: 17.0.31 - resolution: "@types/node@npm:17.0.31" - checksum: 10/707f1b81b84a29428f5a61cb11906621a783cce75767a6d5e1158d4f8a9fe6b2375a9c585d04ebe325f1a78630fe05142f93cf8992142e96de14b09dbad330da - languageName: node - linkType: hard - -"@types/node@npm:>=13.7.0, @types/node@npm:>=20.12.12": - version: 20.14.2 - resolution: "@types/node@npm:20.14.2" +"@types/node@npm:22.16.2": + version: 22.16.2 + resolution: "@types/node@npm:22.16.2" dependencies: - undici-types: "npm:~5.26.4" - checksum: 10/c38e47b190fa0a8bdfde24b036dddcf9401551f2fb170a90ff33625c7d6f218907e81c74e0fa6e394804a32623c24c60c50e249badc951007830f0d02c48ee0f - languageName: node - linkType: hard - -"@types/node@npm:>=8.1.0": - version: 22.7.5 - resolution: "@types/node@npm:22.7.5" - dependencies: - undici-types: "npm:~6.19.2" - checksum: 10/e8ba102f8c1aa7623787d625389be68d64e54fcbb76d41f6c2c64e8cf4c9f4a2370e7ef5e5f1732f3c57529d3d26afdcb2edc0101c5e413a79081449825c57ac - languageName: node - linkType: hard - -"@types/node@npm:^13.7.0": - version: 13.13.52 - resolution: "@types/node@npm:13.13.52" - checksum: 10/a1fbd080dd2462f6f0d0c10cb8328ee6b22e59941fb6beb8bca907f96e00798ce85e94320ccab3bf04f87d6c5443535a62e6896ac59c34c79a286821223e56cd - languageName: node - linkType: hard - -"@types/node@npm:^18.0.0": - version: 18.19.29 - resolution: "@types/node@npm:18.19.29" - dependencies: - undici-types: "npm:~5.26.4" - checksum: 10/9a3572b488f875ca1b545cc96980f1cb54dd05da16b2dc0cc3c3cb49ceafc3a5e417f4741c711c7bb81a67a0ddd29f546dcb077e4cb9b98a492fbaf373b1fbdc - languageName: node - linkType: hard - -"@types/node@npm:^18.17.5": - version: 18.18.13 - resolution: "@types/node@npm:18.18.13" - dependencies: - undici-types: "npm:~5.26.4" - checksum: 10/5dcab799e39570a858741a13373a584529d0e6b81120c8a2118e158749d9ace291748644d760af554fe73ab3cebdd91500314bf1ecd17a746fabcae06ebf9eea - languageName: node - linkType: hard - -"@types/node@npm:^18.19.38": - version: 18.19.39 - resolution: "@types/node@npm:18.19.39" - dependencies: - undici-types: "npm:~5.26.4" - checksum: 10/d2fe84adf087a4184217b666f675e99678060d15f84882a4a1c3e49c3dca521a7e99a201a3c073c2b60b00419f1f4c3b357d8f7397f65e400dc3b77b0145a1da + undici-types: "npm:~6.21.0" + checksum: 10/ff15825a699863e54c9669e888b6d12848180f4f0b88cb9dc84c3dc6f91ca2d1d09df38f4f02ba594e70ade6c53d840f090c4817d8df5968df653921095d77b7 languageName: node linkType: hard @@ -19975,12 +19974,12 @@ __metadata: languageName: node linkType: hard -"@types/yargs@npm:^17.0.10": - version: 17.0.10 - resolution: "@types/yargs@npm:17.0.10" +"@types/yargs@npm:^17.0.33": + version: 17.0.33 + resolution: "@types/yargs@npm:17.0.33" dependencies: "@types/yargs-parser": "npm:*" - checksum: 10/cfe94e8ba50364e08d7b3ecb10a7c153762d0e56c571079538bb06b306638d1045e395fc5a745b94519e73798779c761fa386ec13c82306a62349f64d7b9eec1 + checksum: 10/16f6681bf4d99fb671bf56029141ed01db2862e3db9df7fc92d8bea494359ac96a1b4b1c35a836d1e95e665fb18ad753ab2015fc0db663454e8fd4e5d5e2ef91 languageName: node linkType: hard @@ -21285,6 +21284,15 @@ __metadata: languageName: node linkType: hard +"@whatwg-node/promise-helpers@npm:^1.0.0": + version: 1.3.2 + resolution: "@whatwg-node/promise-helpers@npm:1.3.2" + dependencies: + tslib: "npm:^2.6.3" + checksum: 10/22513e7075d2e6e067399f6b3065a1f280d77aab2cc8699fe5bf9496a76ea7ede2cf4d46fad6f033d0ad686f974c52f85335c3dcddd656d1c8700636713f94a9 + languageName: node + linkType: hard + "@wry/caches@npm:^1.0.0": version: 1.0.1 resolution: "@wry/caches@npm:1.0.1" @@ -21996,7 +22004,7 @@ __metadata: languageName: node linkType: hard -"ansi-styles@npm:^6.1.0": +"ansi-styles@npm:^6.1.0, ansi-styles@npm:^6.2.1": version: 6.2.1 resolution: "ansi-styles@npm:6.2.1" checksum: 10/70fdf883b704d17a5dfc9cde206e698c16bcd74e7f196ab821511651aee4f9f76c9514bdfa6ca3a27b5e49138b89cb222a28caf3afe4567570139577f991df32 @@ -22997,7 +23005,7 @@ __metadata: languageName: node linkType: hard -"bindings@npm:^1.4.0": +"bindings@npm:^1.4.0, bindings@npm:^1.5.0": version: 1.5.0 resolution: "bindings@npm:1.5.0" dependencies: @@ -24550,6 +24558,17 @@ __metadata: languageName: node linkType: hard +"cliui@npm:^9.0.1": + version: 9.0.1 + resolution: "cliui@npm:9.0.1" + dependencies: + string-width: "npm:^7.2.0" + strip-ansi: "npm:^7.1.0" + wrap-ansi: "npm:^9.0.0" + checksum: 10/df43d8d1c6e3254cbb64b1905310d5f6672c595496a3cbe76946c6d24777136886470686f2772ac9edfe547a74bb70e8017530b3554715aee119efd7752fc0d9 + languageName: node + linkType: hard + "clone-deep@npm:^4.0.1": version: 4.0.1 resolution: "clone-deep@npm:4.0.1" @@ -26059,6 +26078,16 @@ __metadata: languageName: node linkType: hard +"deasync@npm:^0.1.30": + version: 0.1.30 + resolution: "deasync@npm:0.1.30" + dependencies: + bindings: "npm:^1.5.0" + node-addon-api: "npm:^1.7.1" + checksum: 10/5cb55096b89181c7305284a3eed5df3eb7a47563bb17f85ca7c22df185fb8837ea19a463dafd127dd912fdad9edbb068a1ae3d48d890ba2cfdc80d3fb654a11f + languageName: node + linkType: hard + "debounce@npm:^1.2.0": version: 1.2.1 resolution: "debounce@npm:1.2.1" @@ -27026,6 +27055,13 @@ __metadata: languageName: node linkType: hard +"dset@npm:^3.1.4": + version: 3.1.4 + resolution: "dset@npm:3.1.4" + checksum: 10/6268c9e2049c8effe6e5a1952f02826e8e32468b5ced781f15f8f3b1c290da37626246fec014fbdd1503413f981dff6abd8a4c718ec9952fd45fccb6ac9de43f + languageName: node + linkType: hard + "dtrace-provider@npm:~0.8": version: 0.8.8 resolution: "dtrace-provider@npm:0.8.8" @@ -27266,6 +27302,13 @@ __metadata: languageName: node linkType: hard +"emoji-regex@npm:^10.3.0": + version: 10.4.0 + resolution: "emoji-regex@npm:10.4.0" + checksum: 10/76bb92c5bcf0b6980d37e535156231e4a9d0aa6ab3b9f5eabf7690231d5aa5d5b8e516f36e6804cbdd0f1c23dfef2a60c40ab7bb8aedd890584281a565b97c50 + languageName: node + linkType: hard + "emoji-regex@npm:^8.0.0": version: 8.0.0 resolution: "emoji-regex@npm:8.0.0" @@ -29292,6 +29335,13 @@ __metadata: languageName: node linkType: hard +"extensionless@npm:^1.9.9": + version: 1.9.9 + resolution: "extensionless@npm:1.9.9" + checksum: 10/5d28368b7397629579aca1e8e9c8d101f44eeab693164bb1db6071b01f378ebaba3c8b1c0c900346ba3295e4d595b4060b14450804977228c22695deda788a3d + languageName: node + linkType: hard + "external-editor@npm:^3.0.3": version: 3.1.0 resolution: "external-editor@npm:3.1.0" @@ -30443,13 +30493,6 @@ __metadata: languageName: node linkType: hard -"get-caller-file@npm:^1.0.2": - version: 1.0.3 - resolution: "get-caller-file@npm:1.0.3" - checksum: 10/0b776558c1d94ac131ec0d47bf9da4e00a38e7d3a6cbde534e0e4656c13ead344e69ef7ed2c0bca16620cc2e1e26529f90e2336c8962736517b64890d583a2a0 - languageName: node - linkType: hard - "get-caller-file@npm:^2.0.1, get-caller-file@npm:^2.0.5": version: 2.0.5 resolution: "get-caller-file@npm:2.0.5" @@ -30457,6 +30500,13 @@ __metadata: languageName: node linkType: hard +"get-east-asian-width@npm:^1.0.0": + version: 1.3.0 + resolution: "get-east-asian-width@npm:1.3.0" + checksum: 10/8e8e779eb28701db7fdb1c8cab879e39e6ae23f52dadd89c8aed05869671cee611a65d4f8557b83e981428623247d8bc5d0c7a4ef3ea7a41d826e73600112ad8 + languageName: node + linkType: hard + "get-folder-size@npm:^4.0.0": version: 4.0.0 resolution: "get-folder-size@npm:4.0.0" @@ -31320,14 +31370,14 @@ __metadata: languageName: node linkType: hard -"graphql-scalars@npm:^1.18.0": - version: 1.18.0 - resolution: "graphql-scalars@npm:1.18.0" +"graphql-scalars@npm:^1.24.1": + version: 1.24.1 + resolution: "graphql-scalars@npm:1.24.1" dependencies: - tslib: "npm:~2.4.0" + tslib: "npm:^2.5.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/3c27b5266cda12a6505562e1c38b1e5c1f00e2953dac2e1b618aaac834dbbe3154b7ea640f0ad63baec9bbcde1d177f3de9dc1bd4e6464f25b16a10d27f9eea6 + checksum: 10/e142c60097ef071d26373b67955f22861a9818efd94115ff11355f5f420f19cf3ed7397d10626700801368e71cf269fb697766c5edf9a3353684c91b37ac831a languageName: node linkType: hard @@ -37949,16 +37999,6 @@ __metadata: languageName: node linkType: hard -"mock-require@npm:^3.0.3": - version: 3.0.3 - resolution: "mock-require@npm:3.0.3" - dependencies: - get-caller-file: "npm:^1.0.2" - normalize-path: "npm:^2.1.1" - checksum: 10/4ae5bfd19428e47fadbf4d830d22aba28af237dab4d6e76a5bb49cac6415c2c7646acd66fd50cefce277a3470e413146c345a68ce236cf3047b340a06bcdc2b2 - languageName: node - linkType: hard - "mock-socket@npm:^9.3.1": version: 9.3.1 resolution: "mock-socket@npm:9.3.1" @@ -37987,6 +38027,15 @@ __metadata: languageName: node linkType: hard +"moq.ts@npm:10.0.8": + version: 10.0.8 + resolution: "moq.ts@npm:10.0.8" + dependencies: + tslib: "npm:*" + checksum: 10/d2dce5da03f1290dee657504ead55fd6a82065627ec4d9fa193827e7b3b4f3c67dcb81826b52b63af7dc062b73f8213750d3e4063af3ceb032ae91c5bbe31789 + languageName: node + linkType: hard + "mri@npm:^1.2.0": version: 1.2.0 resolution: "mri@npm:1.2.0" @@ -38382,6 +38431,15 @@ __metadata: languageName: node linkType: hard +"node-addon-api@npm:^1.7.1": + version: 1.7.2 + resolution: "node-addon-api@npm:1.7.2" + dependencies: + node-gyp: "npm:latest" + checksum: 10/6bf8217a8cd8148f4bbfd319b46d33587e9fb2e63e3c856ded67a76715167f7a6b17e1d9b8bbf3b8508befeb6a4adb10d92b8998ed5c19ca8448343f4cea11d6 + languageName: node + linkType: hard + "node-addon-api@npm:^5.0.0": version: 5.1.0 resolution: "node-addon-api@npm:5.1.0" @@ -45808,6 +45866,17 @@ __metadata: languageName: node linkType: hard +"string-width@npm:^7.0.0, string-width@npm:^7.2.0": + version: 7.2.0 + resolution: "string-width@npm:7.2.0" + dependencies: + emoji-regex: "npm:^10.3.0" + get-east-asian-width: "npm:^1.0.0" + strip-ansi: "npm:^7.1.0" + checksum: 10/42f9e82f61314904a81393f6ef75b832c39f39761797250de68c041d8ba4df2ef80db49ab6cd3a292923a6f0f409b8c9980d120f7d32c820b4a8a84a2598a295 + languageName: node + linkType: hard + "string.prototype.trimend@npm:^1.0.4, string.prototype.trimend@npm:^1.0.5": version: 1.0.5 resolution: "string.prototype.trimend@npm:1.0.5" @@ -45873,6 +45942,15 @@ __metadata: languageName: node linkType: hard +"strip-ansi@npm:^7.1.0": + version: 7.1.0 + resolution: "strip-ansi@npm:7.1.0" + dependencies: + ansi-regex: "npm:^6.0.1" + checksum: 10/475f53e9c44375d6e72807284024ac5d668ee1d06010740dec0b9744f2ddf47de8d7151f80e5f6190fc8f384e802fdf9504b76a7e9020c9faee7103623338be2 + languageName: node + linkType: hard + "strip-bom@npm:4.0.0, strip-bom@npm:^4.0.0": version: 4.0.0 resolution: "strip-bom@npm:4.0.0" @@ -47950,17 +48028,10 @@ __metadata: languageName: node linkType: hard -"undici-types@npm:~5.26.4": - version: 5.26.5 - resolution: "undici-types@npm:5.26.5" - checksum: 10/0097779d94bc0fd26f0418b3a05472410408877279141ded2bd449167be1aed7ea5b76f756562cb3586a07f251b90799bab22d9019ceba49c037c76445f7cddd - languageName: node - linkType: hard - -"undici-types@npm:~6.19.2": - version: 6.19.8 - resolution: "undici-types@npm:6.19.8" - checksum: 10/cf0b48ed4fc99baf56584afa91aaffa5010c268b8842f62e02f752df209e3dea138b372a60a963b3b2576ed932f32329ce7ddb9cb5f27a6c83040d8cd74b7a70 +"undici-types@npm:~6.21.0": + version: 6.21.0 + resolution: "undici-types@npm:6.21.0" + checksum: 10/ec8f41aa4359d50f9b59fa61fe3efce3477cc681908c8f84354d8567bb3701fafdddf36ef6bff307024d3feb42c837cf6f670314ba37fc8145e219560e473d14 languageName: node linkType: hard @@ -50123,6 +50194,15 @@ __metadata: languageName: node linkType: hard +"why-is-node-running@npm:^3.2.2": + version: 3.2.2 + resolution: "why-is-node-running@npm:3.2.2" + bin: + why-is-node-running: cli.js + checksum: 10/b57146897f676cf01fe30ac415f66dd72b54218e6fee8eb4479251f72a2e064bc18ed7eaddac18513f978353ce2b758d16a4de126cd3e47e467631c1ec5a50af + languageName: node + linkType: hard + "wide-align@npm:^1.1.2, wide-align@npm:^1.1.5": version: 1.1.5 resolution: "wide-align@npm:1.1.5" @@ -50259,6 +50339,17 @@ __metadata: languageName: node linkType: hard +"wrap-ansi@npm:^9.0.0": + version: 9.0.0 + resolution: "wrap-ansi@npm:9.0.0" + dependencies: + ansi-styles: "npm:^6.2.1" + string-width: "npm:^7.0.0" + strip-ansi: "npm:^7.1.0" + checksum: 10/b9d91564c091cf3978a7c18ca0f3e4d4606e83549dbe59cf76f5e77feefdd5ec91443155e8102630524d10a8c275efac8a7082c0f26fa43e6b989dc150d176ce + languageName: node + linkType: hard + "wrappy@npm:1": version: 1.0.2 resolution: "wrappy@npm:1.0.2" @@ -50591,6 +50682,13 @@ __metadata: languageName: node linkType: hard +"yargs-parser@npm:^22.0.0": + version: 22.0.0 + resolution: "yargs-parser@npm:22.0.0" + checksum: 10/f13c42bad6ebed1a587a72f2db5694f5fa772bcaf409a701691d13cf74eb5adfcf61a2611de08807e319b829d3e5e6e1578b16ebe174cae8e8be3bf7b8e7a19e + languageName: node + linkType: hard + "yargs-unparser@npm:2.0.0": version: 2.0.0 resolution: "yargs-unparser@npm:2.0.0" @@ -50697,6 +50795,20 @@ __metadata: languageName: node linkType: hard +"yargs@npm:^18.0.0": + version: 18.0.0 + resolution: "yargs@npm:18.0.0" + dependencies: + cliui: "npm:^9.0.1" + escalade: "npm:^3.1.1" + get-caller-file: "npm:^2.0.5" + string-width: "npm:^7.2.0" + y18n: "npm:^5.0.5" + yargs-parser: "npm:^22.0.0" + checksum: 10/5af36234871390386b31cac99f00e79fcbc2ead858a61b30a8ca381c5fde5df8af0b407c36b000d3f774bcbe4aec5833f2f1c915f6ddc49ce97b78176b651801 + languageName: node + linkType: hard + "yauzl@npm:^2.10.0": version: 2.10.0 resolution: "yauzl@npm:2.10.0"