chore(server): migrating fully to ESM (#5042)

* wip

* some extra fixes

* stuff kinda works?

* need to figure out mocks

* need to figure out mocks

* fix db listener

* gqlgen fix

* minor gqlgen watch adjustment

* lint fixes

* delete old codegen file

* converting migrations to ESM

* getModuleDIrectory

* vitest sort of works

* added back ts-vitest

* resolve gql double load

* fixing test timeout configs

* TSC lint fix

* fix automate tests

* moar debugging

* debugging

* more debugging

* codegen update

* server works

* yargs migrated

* chore(server): getting rid of global mocks for Server ESM (#5046)

* got rid of email mock

* got rid of comment mocks

* got rid of multi region mocks

* got rid of stripe mock

* admin override mock updated

* removed final mock

* fixing import.meta.resolve calls

* another import.meta.resolve fix

* added requested test

* nyc ESM fix

* removed unneeded deps + linting

* yarn lock forgot to commit

* tryna fix flakyness

* email capture util fix

* sendEmail fix

* fix TSX check

* sender transporter fix + CR comments

* merge main fix

* test fixx

* circleci fix

* gqlgen bigint fix

* error formatter fix

* more error formatting improvements

* esmloader added to Dockerfile

* more dockerfile fixes

* bg jobs fix
This commit is contained in:
Kristaps Fabians Geikins
2025-07-14 10:26:19 +03:00
committed by GitHub
parent 520e931211
commit bde148f286
348 changed files with 2599 additions and 2067 deletions
+1 -17
View File
@@ -527,10 +527,6 @@ jobs:
- run: - run:
command: cp .env.test-example .env.test command: cp .env.test-example .env.test
working_directory: 'packages/server' working_directory: 'packages/server'
- run:
name: 'Lint'
command: yarn lint:ci
working_directory: 'packages/server'
- run: - run:
name: 'Run tests' name: 'Run tests'
# Extra formatting to get timestamps on each line in CI (for profiling purposes) # Extra formatting to get timestamps on each line in CI (for profiling purposes)
@@ -546,18 +542,6 @@ jobs:
no_output_timeout: 30m no_output_timeout: 30m
- codecov/upload: - codecov/upload:
files: packages/server/coverage/lcov.info 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: - store_test_results:
path: packages/server/reports path: packages/server/reports
@@ -591,7 +575,7 @@ jobs:
test-server-multiregion: test-server-multiregion:
<<: *test-server-job <<: *test-server-job
docker: docker:
- image: cimg/node:18.19.0 - image: cimg/node:22.6.0
- image: cimg/redis:7.2.4 - image: cimg/redis:7.2.4
- image: 'speckle/speckle-postgres' - image: 'speckle/speckle-postgres'
environment: environment:
+2
View File
@@ -78,9 +78,11 @@ bin/
!packages/monitor-deployment/bin !packages/monitor-deployment/bin
!packages/preview-service/bin !packages/preview-service/bin
!packages/server/bin !packages/server/bin
!packages/server/modules/cli/bin
!packages/viewer/src/modules/loaders/OBJ !packages/viewer/src/modules/loaders/OBJ
# Server # Server
multiregion.json multiregion.json
multiregion.test.json multiregion.test.json
packages/*/.tshy/ packages/*/.tshy/
.vite-node
+2 -1
View File
@@ -95,7 +95,8 @@
"typescript": "^5.7.3", "typescript": "^5.7.3",
"typescript-eslint": "^8.20.0", "typescript-eslint": "^8.20.0",
"wait-on": ">=7.2.0", "wait-on": ">=7.2.0",
"vitest": "^3.0.7" "vitest": "^3.0.7",
"@types/node": "22.16.2"
}, },
"config": { "config": {
"commitizen": { "commitizen": {
@@ -13,7 +13,7 @@ import {
import { CameraController, ViewMode, VisualDiffMode } from '@speckle/viewer' import { CameraController, ViewMode, VisualDiffMode } from '@speckle/viewer'
import type { NumericPropertyInfo } from '@speckle/viewer' import type { NumericPropertyInfo } from '@speckle/viewer'
import type { PartialDeep } from 'type-fest' 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 type SerializedViewerState = SpeckleViewer.ViewerState.SerializedViewerState
@@ -65,7 +65,7 @@ import {
import { useSynchronizedCookie } from '~~/lib/common/composables/reactiveCookie' import { useSynchronizedCookie } from '~~/lib/common/composables/reactiveCookie'
import { buildManualPromise } from '@speckle/ui-components' import { buildManualPromise } from '@speckle/ui-components'
import { PassReader } from '../extensions/PassReader' 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< export type LoadedModel = NonNullable<
Get<ViewerLoadedResourcesQuery, 'project.models.items[0]'> Get<ViewerLoadedResourcesQuery, 'project.models.items[0]'>
@@ -52,7 +52,7 @@ import {
import { setupDebugMode } from '~~/lib/viewer/composables/setup/dev' import { setupDebugMode } from '~~/lib/viewer/composables/setup/dev'
import { useEmbed } from '~/lib/viewer/composables/setup/embed' import { useEmbed } from '~/lib/viewer/composables/setup/embed'
import { useMixpanel } from '~~/lib/core/composables/mp' 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() { function useViewerIsBusyEventHandler() {
const state = useInjectedViewerState() const state = useInjectedViewerState()
+3 -1
View File
@@ -73,6 +73,8 @@ COPY --link --from=dependency-stage /speckle-server/node_modules ./node_modules
WORKDIR /speckle-server/packages/server 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/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/dist ./dist
COPY --link --from=build-stage /speckle-server/packages/server/assets ./assets COPY --link --from=build-stage /speckle-server/packages/server/assets ./assets
COPY --link --from=build-stage /speckle-server/packages/server/bin ./bin COPY --link --from=build-stage /speckle-server/packages/server/bin ./bin
@@ -86,4 +88,4 @@ ARG NODE_ENV
ENV NODE_ENV=${NODE_ENV} \ ENV NODE_ENV=${NODE_ENV} \
SPECKLE_SERVER_VERSION=${SPECKLE_SERVER_VERSION} SPECKLE_SERVER_VERSION=${SPECKLE_SERVER_VERSION}
ENTRYPOINT [ "tini", "--", "/nodejs/bin/node", "./bin/www" ] ENTRYPOINT [ "tini", "--", "/nodejs/bin/node", "--import=./esmLoader.js", "./bin/www" ]
+8 -9
View File
@@ -1,7 +1,7 @@
/* eslint-disable camelcase */ /* eslint-disable camelcase */
/* istanbul ignore file */ /* istanbul ignore file */
// eslint-disable-next-line no-restricted-imports // eslint-disable-next-line no-restricted-imports
import './bootstrap' import './bootstrap.js'
import http from 'http' import http from 'http'
import express, { Express } from 'express' import express, { Express } from 'express'
@@ -54,7 +54,7 @@ import {
import * as ModulesSetup from '@/modules/index' import * as ModulesSetup from '@/modules/index'
import { GraphQLContext, Optional } from '@/modules/shared/helpers/typeHelper' 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 { corsMiddlewareFactory } from '@/modules/core/configs/cors'
import { import {
authContextMiddleware, 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 * 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 * 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 server: http.Server | MockWsServer
registers?: Registry[] registers?: Registry[]
}): SubscriptionServer { }): Promise<SubscriptionServer> {
const { server, registers } = params const { server, registers } = params
const httpServer = isWsServer(server) ? undefined : server const httpServer = isWsServer(server) ? undefined : server
const mockServer = isWsServer(server) ? server : undefined const mockServer = isWsServer(server) ? server : undefined
// we have to break the type here, cause its a mock // we have to break the type here, cause its a mock
const wsServer = mockServer ? (mockServer as unknown as ws.Server) : undefined const wsServer = mockServer ? (mockServer as unknown as ws.Server) : undefined
const schema = ModulesSetup.graphSchema() const schema = await ModulesSetup.graphSchema()
const { const {
metricConnectCounter, metricConnectCounter,
@@ -250,7 +250,7 @@ export async function buildApolloServer(options?: {
}): Promise<ApolloServer<GraphQLContext>> { }): Promise<ApolloServer<GraphQLContext>> {
const includeStacktraceInErrorResponses = isDevEnv() || isTestEnv() const includeStacktraceInErrorResponses = isDevEnv() || isTestEnv()
const subscriptionServer = options?.subscriptionServer const subscriptionServer = options?.subscriptionServer
const schema = ModulesSetup.graphSchema(await buildMocksConfig()) const schema = await ModulesSetup.graphSchema(await buildMocksConfig())
const server = new ApolloServer({ const server = new ApolloServer({
schema, schema,
@@ -356,7 +356,7 @@ export async function init() {
// Init HTTP server & subscription server // Init HTTP server & subscription server
const server = http.createServer(app) const server = http.createServer(app)
const subscriptionServer = buildApolloSubscriptionServer({ const subscriptionServer = await buildApolloSubscriptionServer({
server, server,
registers: [promRegister] registers: [promRegister]
}) })
@@ -398,8 +398,7 @@ const shouldUseFrontendProxy = () => isDevEnv()
async function createFrontendProxy() { async function createFrontendProxy() {
const frontendHost = process.env.FRONTEND_HOST || '127.0.0.1' const frontendHost = process.env.FRONTEND_HOST || '127.0.0.1'
const frontendPort = process.env.FRONTEND_PORT || 8081 const frontendPort = process.env.FRONTEND_PORT || 8081
const { createProxyMiddleware } = const { createProxyMiddleware } = await import('http-proxy-middleware')
require('http-proxy-middleware') as typeof import('http-proxy-middleware')
// even tho it has default values, it fixes http-proxy setting `Connection: close` on each request // even tho it has default values, it fixes http-proxy setting `Connection: close` on each request
// slowing everything down // slowing everything down
+13
View File
@@ -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)
+3 -4
View File
@@ -1,6 +1,5 @@
#!/usr/bin/env node #!/usr/bin/env node
'use strict' import path from 'path'
const path = require('path')
/** /**
* Find mocha and run it (we don't want to hardcode a specific node_modules 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 relativeBinPath = './bin/mocha.js'
const mochaPath = require.resolve('mocha') const mochaPath = import.meta.resolve('mocha')
const mochaPathDir = path.dirname(mochaPath) const mochaPathDir = path.dirname(mochaPath)
const mochaBinPath = path.join(mochaPathDir, relativeBinPath) const mochaBinPath = path.join(mochaPathDir, relativeBinPath)
require(mochaBinPath) await import(mochaBinPath)
-24
View File
@@ -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)
})
// 💥
+2 -2
View File
@@ -1,8 +1,8 @@
#!/usr/bin/env node #!/usr/bin/env node
'use strict' 'use strict'
const { logger } = require('../dist/observability/logging') import { logger } from '../dist/observability/logging.js'
const { init, startHttp } = require('../dist/app') import { init, startHttp } from '../dist/app.js'
init() init()
.then(({ app, graphqlServer, registers, server, readinessCheck }) => .then(({ app, graphqlServer, registers, server, readinessCheck }) =>
+16 -33
View File
@@ -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 * 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 // 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()) { if (isApolloMonitoringEnabled() && !getApolloServerVersion()) {
process.env.APOLLO_SERVER_USER_VERSION = getServerVersion() process.env.APOLLO_SERVER_USER_VERSION = getServerVersion()
} }
// If running in test env, load .env.test first // 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()) { if (isTestEnv()) {
const { error } = dotenv.config({ path: `${packageRoot}/.env.test` }) const { error } = dotenv.config({ path: `${packageRoot}/.env.test` })
if (error) { if (error) {
@@ -48,7 +38,6 @@ if (isTestEnv()) {
// (e.g. due to various child processes capturing the --inspect flag) // (e.g. due to various child processes capturing the --inspect flag)
const startDebugger = process.env.START_DEBUGGER const startDebugger = process.env.START_DEBUGGER
if ((isTestEnv() || isDevEnv()) && startDebugger) { if ((isTestEnv() || isDevEnv()) && startDebugger) {
const inspector = require('node:inspector')
if (!inspector.url()) { if (!inspector.url()) {
console.log('Debugger starting on process ' + process.pid) console.log('Debugger starting on process ' + process.pid)
inspector.open(0, undefined, true) inspector.open(0, undefined, true)
@@ -58,13 +47,7 @@ if ((isTestEnv() || isDevEnv()) && startDebugger) {
dotenv.config({ path: `${packageRoot}/.env` }) dotenv.config({ path: `${packageRoot}/.env` })
// knex is a singleton controlled by module so can't wait til app init // knex is a singleton controlled by module so can't wait til app init
const { initOpenTelemetry } = require('@/observability/otel')
initOpenTelemetry() initOpenTelemetry()
const { patchKnex } = require('@/modules/core/patches/knex')
patchKnex() patchKnex()
module.exports = { export { appRoot, packageRoot }
appRoot,
packageRoot
}
+214
View File
@@ -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<string, unknown>',
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<string, unknown>',
DateTime: 'string'
}
}
}
},
config: {
enumsAsConst: true,
scalars: {
JSONObject: 'Record<string, unknown>',
DateTime: 'Date'
}
}
}
export default config
-139
View File
@@ -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<string, unknown>
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<string, unknown>
DateTime: string
config:
enumsAsConst: true
scalars:
JSONObject: Record<string, unknown>
DateTime: Date
require:
- ts-node/register
- tsconfig-paths/register
+5 -5
View File
@@ -13,24 +13,24 @@ const configs = [
...baseConfigs, ...baseConfigs,
{ {
languageOptions: { languageOptions: {
sourceType: 'commonjs', sourceType: 'module',
globals: { globals: {
...globals.node ...globals.node
} }
} }
}, },
{ {
files: ['**/*.mjs'], files: ['**/*.cjs', '**/*.cts'],
languageOptions: { languageOptions: {
sourceType: 'module' sourceType: 'commonjs'
} }
}, },
...tseslint.configs.recommendedTypeChecked.map((c) => ({ ...tseslint.configs.recommendedTypeChecked.map((c) => ({
...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: { languageOptions: {
parserOptions: { parserOptions: {
tsconfigRootDir: getESMDirname(import.meta.url), tsconfigRootDir: getESMDirname(import.meta.url),
+84
View File
@@ -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
}
+3 -3
View File
@@ -1,6 +1,6 @@
import { ensureErrorOrWrapAsCause } from '@/modules/shared/errors/ensureError' import { ensureErrorOrWrapAsCause } from '@/modules/shared/errors/ensureError'
import { join, merge } from 'lodash' import { join, merge } from 'lodash-es'
import { MultiError } from 'verror' import VError from 'verror'
import { import {
FreeConnectionsCalculators, FreeConnectionsCalculators,
MultiDBCheck, MultiDBCheck,
@@ -30,7 +30,7 @@ export const handleLivenessFactory =
', ' ', '
)} is not available.`, )} is not available.`,
{ {
cause: new MultiError( cause: new VError.MultiError(
Object.entries(allPostgresResults).map((kv) => Object.entries(allPostgresResults).map((kv) =>
ensureErrorOrWrapAsCause( ensureErrorOrWrapAsCause(
//HACK: kv[1] is not typed correctly as the filter does not narrow the type //HACK: kv[1] is not typed correctly as the filter does not narrow the type
+1 -1
View File
@@ -1,6 +1,6 @@
/* eslint-disable no-restricted-imports */ /* eslint-disable no-restricted-imports */
/* istanbul ignore file */ /* istanbul ignore file */
import { packageRoot } from './bootstrap' import { packageRoot } from './bootstrap.js'
import fs from 'fs' import fs from 'fs'
import path from 'path' import path from 'path'
import { import {
@@ -268,4 +268,4 @@ const resolvers: Resolvers = {
} }
} }
export = resolvers export default resolvers
@@ -26,4 +26,4 @@ const ServerAccessRequestsModule: SpeckleModule = {
} }
} }
export = ServerAccessRequestsModule export default ServerAccessRequestsModule
@@ -51,15 +51,15 @@ import {
} from '@/test/graphql/generated/graphql' } from '@/test/graphql/generated/graphql'
import { testApolloServer, TestApolloServer } from '@/test/graphqlHelper' import { testApolloServer, TestApolloServer } from '@/test/graphqlHelper'
import { truncateTables } from '@/test/hooks' import { truncateTables } from '@/test/hooks'
import { EmailSendingServiceMock } from '@/test/mocks/global'
import { import {
buildNotificationsStateTracker, buildNotificationsStateTracker,
NotificationsStateManager NotificationsStateManager
} from '@/test/notificationsHelper' } from '@/test/notificationsHelper'
import { getStreamActivities } from '@/test/speckle-helpers/activityStreamHelper' import { getStreamActivities } from '@/test/speckle-helpers/activityStreamHelper'
import { createEmailListener, TestEmailListener } from '@/test/speckle-helpers/email'
import { BasicTestStream, createTestStreams } from '@/test/speckle-helpers/streamHelper' import { BasicTestStream, createTestStreams } from '@/test/speckle-helpers/streamHelper'
import { expect } from 'chai' import { expect } from 'chai'
import { noop } from 'lodash' import { noop } from 'lodash-es'
const getUser = getUserFactory({ db }) const getUser = getUserFactory({ db })
const getStream = getStreamFactory({ db }) const getStream = getStreamFactory({ db })
@@ -150,6 +150,8 @@ describe('Project access requests', () => {
id: '' id: ''
} }
let emailListener: TestEmailListener
let quitters: (() => void)[] = [] let quitters: (() => void)[] = []
before(async () => { before(async () => {
@@ -164,15 +166,18 @@ describe('Project access requests', () => {
authUserId: me.id authUserId: me.id
}) })
notificationsStateManager = buildNotificationsStateTracker() notificationsStateManager = buildNotificationsStateTracker()
emailListener = await createEmailListener()
}) })
afterEach(() => { afterEach(() => {
emailListener.reset()
quitters.forEach((q) => q()) quitters.forEach((q) => q())
quitters = [] quitters = []
}) })
after(async () => { after(async () => {
notificationsStateManager.destroy() notificationsStateManager.destroy()
await emailListener.destroy()
}) })
const createReq = async (projectId: string) => const createReq = async (projectId: string) =>
@@ -228,10 +233,8 @@ describe('Project access requests', () => {
eventFired = true eventFired = true
}) })
) )
const sendEmailCall = EmailSendingServiceMock.hijackFunction(
'sendEmail', const { getSends } = emailListener.listen({ times: 1 })
async () => true
)
const waitForAck = notificationsStateManager.waitForAck( const waitForAck = notificationsStateManager.waitForAck(
(e) => e.result?.type === NotificationType.NewStreamAccessRequest (e) => e.result?.type === NotificationType.NewStreamAccessRequest
@@ -255,8 +258,9 @@ describe('Project access requests', () => {
await waitForAck await waitForAck
// email gets sent out // email gets sent out
expect(sendEmailCall.args?.[0]?.[0]).to.be.ok const sentEmails = getSends()
const emailParams = sendEmailCall.args[0][0] expect(sentEmails.length).to.eq(1)
const emailParams = sentEmails[0]
expect(emailParams.subject).to.contain('A user requested access to your project') expect(emailParams.subject).to.contain('A user requested access to your project')
expect(emailParams.html).to.be.ok expect(emailParams.html).to.be.ok
@@ -52,15 +52,15 @@ import {
import { StreamRole } from '@/test/graphql/generated/graphql' import { StreamRole } from '@/test/graphql/generated/graphql'
import { createAuthedTestContext, ServerAndContext } from '@/test/graphqlHelper' import { createAuthedTestContext, ServerAndContext } from '@/test/graphqlHelper'
import { truncateTables } from '@/test/hooks' import { truncateTables } from '@/test/hooks'
import { EmailSendingServiceMock } from '@/test/mocks/global'
import { import {
buildNotificationsStateTracker, buildNotificationsStateTracker,
NotificationsStateManager NotificationsStateManager
} from '@/test/notificationsHelper' } from '@/test/notificationsHelper'
import { getStreamActivities } from '@/test/speckle-helpers/activityStreamHelper' import { getStreamActivities } from '@/test/speckle-helpers/activityStreamHelper'
import { createEmailListener, TestEmailListener } from '@/test/speckle-helpers/email'
import { BasicTestStream, createTestStreams } from '@/test/speckle-helpers/streamHelper' import { BasicTestStream, createTestStreams } from '@/test/speckle-helpers/streamHelper'
import { expect } from 'chai' import { expect } from 'chai'
import { noop } from 'lodash' import { noop } from 'lodash-es'
const getUser = getUserFactory({ db }) const getUser = getUserFactory({ db })
const getStreamCollaborators = getStreamCollaboratorsFactory({ db }) const getStreamCollaborators = getStreamCollaboratorsFactory({ db })
@@ -154,6 +154,8 @@ describe('Stream access requests', () => {
id: '' id: ''
} }
let emailListener: TestEmailListener
before(async () => { before(async () => {
await cleanup() await cleanup()
await createTestUsers([me, otherGuy, anotherGuy]) await createTestUsers([me, otherGuy, anotherGuy])
@@ -167,10 +169,16 @@ describe('Stream access requests', () => {
context: await createAuthedTestContext(me.id) context: await createAuthedTestContext(me.id)
} }
notificationsStateManager = buildNotificationsStateTracker() notificationsStateManager = buildNotificationsStateTracker()
emailListener = await createEmailListener()
}) })
after(async () => { after(async () => {
notificationsStateManager.destroy() notificationsStateManager.destroy()
await emailListener.destroy()
})
afterEach(async () => {
emailListener.reset()
}) })
const createReq = (streamId: string) => const createReq = (streamId: string) =>
@@ -202,10 +210,7 @@ describe('Stream access requests', () => {
}) })
it('operation succeeds', async () => { it('operation succeeds', async () => {
const sendEmailCall = EmailSendingServiceMock.hijackFunction( const { getSends } = emailListener.listen({ times: 1 })
'sendEmail',
async () => true
)
const waitForAck = notificationsStateManager.waitForAck( const waitForAck = notificationsStateManager.waitForAck(
(e) => e.result?.type === NotificationType.NewStreamAccessRequest (e) => e.result?.type === NotificationType.NewStreamAccessRequest
@@ -226,8 +231,9 @@ describe('Stream access requests', () => {
await waitForAck await waitForAck
// email gets sent out // email gets sent out
expect(sendEmailCall.args?.[0]?.[0]).to.be.ok const sentEmails = getSends()
const emailParams = sendEmailCall.args[0][0] expect(sentEmails.length).to.eq(1)
const emailParams = sentEmails[0]
expect(emailParams.subject).to.contain('A user requested access to your project') expect(emailParams.subject).to.contain('A user requested access to your project')
expect(emailParams.html).to.be.ok expect(emailParams.html).to.be.ok
@@ -15,7 +15,7 @@ import {
import { CommentEvents, CommentEventsPayloads } from '@/modules/comments/domain/events' import { CommentEvents, CommentEventsPayloads } from '@/modules/comments/domain/events'
import { ReplyCreateInput } from '@/modules/core/graph/generated/graphql' import { ReplyCreateInput } from '@/modules/core/graph/generated/graphql'
import { EventBusListen } from '@/modules/shared/services/eventBus' import { EventBusListen } from '@/modules/shared/services/eventBus'
import { has } from 'lodash' import { has } from 'lodash-es'
import { OverrideProperties } from 'type-fest' import { OverrideProperties } from 'type-fest'
const addThreadCreatedActivityFactory = const addThreadCreatedActivityFactory =
@@ -65,7 +65,7 @@ const userTimelineQueryCore = async (
return { items, cursor, totalCount } return { items, cursor, totalCount }
} }
export = { export default {
LimitedUser: { LimitedUser: {
async activity(parent, args) { async activity(parent, args) {
return await userActivityQueryCore(parent, args) return await userActivityQueryCore(parent, args)
@@ -154,6 +154,6 @@ const activityModule: SpeckleModule = {
} }
} }
export = { export default {
...activityModule ...activityModule
} }
@@ -1,5 +1,5 @@
// /* istanbul ignore file */ // /* istanbul ignore file */
exports.up = async (knex) => { const up = async (knex) => {
await knex.schema.createTable('stream_activity', (table) => { 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 // 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) 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') await knex.schema.dropTableIfExists('stream_activity')
} }
export { up, down }
+2 -1
View File
@@ -2,10 +2,11 @@ import { SpeckleModule } from '@/modules/shared/helpers/typeHelper'
import { moduleLogger } from '@/observability/logging' import { moduleLogger } from '@/observability/logging'
import { readFile } from 'fs/promises' import { readFile } from 'fs/promises'
import { getFrontendOrigin } from '@/modules/shared/helpers/envHelper' import { getFrontendOrigin } from '@/modules/shared/helpers/envHelper'
import { fileURLToPath } from 'url'
async function getExplorerHtml() { async function getExplorerHtml() {
const fileBaseContents = await readFile( const fileBaseContents = await readFile(
require.resolve('#/assets/apiexplorer/templates/explorer.html'), fileURLToPath(import.meta.resolve('#/assets/apiexplorer/templates/explorer.html')),
{ encoding: 'utf-8' } { encoding: 'utf-8' }
) )
return fileBaseContents.replace( return fileBaseContents.replace(
@@ -25,7 +25,7 @@ const revokeExistingAppCredentialsForUser = revokeExistingAppCredentialsForUserF
db db
}) })
export = { export default {
Query: { Query: {
async app(_parent, args) { async app(_parent, args) {
const app = await getApp({ id: args.id }) const app = await getApp({ id: args.id })
@@ -1,7 +1,7 @@
import { getAuthStrategies } from '@/modules/auth' import { getAuthStrategies } from '@/modules/auth'
import { Resolvers } from '@/modules/core/graph/generated/graphql' import { Resolvers } from '@/modules/core/graph/generated/graphql'
export = { export default {
ServerInfo: { ServerInfo: {
authStrategies() { authStrategies() {
return getAuthStrategies() return getAuthStrategies()
@@ -1,5 +1,4 @@
import { expect } from 'chai' import { expect } from 'chai'
import { describe, it } from 'mocha'
import { getNameFromUserInfo } from '@/modules/auth/helpers/oidc' import { getNameFromUserInfo } from '@/modules/auth/helpers/oidc'
/* eslint-disable camelcase */ /* eslint-disable camelcase */
+1 -1
View File
@@ -8,7 +8,7 @@ import {
getFrontendOrigin, getFrontendOrigin,
getSessionSecret getSessionSecret
} from '@/modules/shared/helpers/envHelper' } from '@/modules/shared/helpers/envHelper'
import { isString, noop } from 'lodash' import { isString, noop } from 'lodash-es'
import { CreateAuthorizationCode } from '@/modules/auth/domain/operations' import { CreateAuthorizationCode } from '@/modules/auth/domain/operations'
import { ensureError, TIME_MS } from '@speckle/shared' import { ensureError, TIME_MS } from '@speckle/shared'
import { LegacyGetUser } from '@/modules/core/domain/users/operations' import { LegacyGetUser } from '@/modules/core/domain/users/operations'
@@ -2,7 +2,7 @@
'use strict' 'use strict'
// Knex table migrations // Knex table migrations
exports.up = async (knex) => { const up = async (knex) => {
// Applications that integrate with this server. // Applications that integrate with this server.
await knex.schema.createTable('server_apps', (table) => { await knex.schema.createTable('server_apps', (table) => {
table.string('id', 10).primary() table.string('id', 10).primary()
@@ -93,7 +93,7 @@ exports.up = async (knex) => {
// await knex( 'scopes' ).insert( appTokenScopes ) // await knex( 'scopes' ).insert( appTokenScopes )
} }
exports.down = async (knex) => { const down = async (knex) => {
await knex.schema.dropTableIfExists('server_apps_scopes') await knex.schema.dropTableIfExists('server_apps_scopes')
await knex.schema.dropTableIfExists('authorization_codes') await knex.schema.dropTableIfExists('authorization_codes')
await knex.schema.dropTableIfExists('refresh_tokens') await knex.schema.dropTableIfExists('refresh_tokens')
@@ -101,3 +101,5 @@ exports.down = async (knex) => {
await knex.schema.dropTableIfExists('server_apps') await knex.schema.dropTableIfExists('server_apps')
} }
export { up, down }
@@ -46,7 +46,7 @@ import {
import { ServerAppRecord, UserRecord } from '@/modules/core/helpers/types' import { ServerAppRecord, UserRecord } from '@/modules/core/helpers/types'
import cryptoRandomString from 'crypto-random-string' import cryptoRandomString from 'crypto-random-string'
import { Knex } from 'knex' import { Knex } from 'knex'
import { difference, omit } from 'lodash' import { difference, omit } from 'lodash-es'
import { AppCreateError } from '@/modules/auth/errors' import { AppCreateError } from '@/modules/auth/errors'
import { UserInputError } from '@/modules/core/errors/userinput' import { UserInputError } from '@/modules/core/errors/userinput'
@@ -8,7 +8,7 @@ import {
import { InvalidArgumentError } from '@/modules/shared/errors' import { InvalidArgumentError } from '@/modules/shared/errors'
import { Nullable } from '@/modules/shared/helpers/typeHelper' import { Nullable } from '@/modules/shared/helpers/typeHelper'
import { ServerAppsScopesRecord } from '@/modules/auth/helpers/types' 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 { TokenScopeData } from '@/modules/shared/domain/rolesAndScopes/types'
import { Knex } from 'knex' import { Knex } from 'knex'
import { import {
@@ -2,7 +2,7 @@ import type { Strategy, AuthenticateOptions } from 'passport'
import passport from 'passport' import passport from 'passport'
import type { Request, Response, NextFunction, RequestHandler } from 'express' import type { Request, Response, NextFunction, RequestHandler } from 'express'
import { ensureError, type Optional, throwUncoveredError } from '@speckle/shared' 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 type { PassportAuthenticateHandlerBuilder } from '@/modules/auth/domain/operations'
import { ExpectedAuthFailure } from '@/modules/auth/domain/const' import { ExpectedAuthFailure } from '@/modules/auth/domain/const'
import type { ResolveAuthRedirectPath } from '@/modules/serverinvites/services/operations' import type { ResolveAuthRedirectPath } from '@/modules/serverinvites/services/operations'
+1 -1
View File
@@ -80,4 +80,4 @@ const setupStrategiesFactory =
return authStrategies return authStrategies
} }
export = setupStrategiesFactory export default setupStrategiesFactory
@@ -223,4 +223,4 @@ const azureAdStrategyBuilderFactory =
} }
} }
export = azureAdStrategyBuilderFactory export default azureAdStrategyBuilderFactory
@@ -17,7 +17,7 @@ import {
getServerOrigin getServerOrigin
} from '@/modules/shared/helpers/envHelper' } from '@/modules/shared/helpers/envHelper'
import type { Request } from 'express' import type { Request } from 'express'
import { get } from 'lodash' import { get } from 'lodash-es'
import { ensureError, Optional } from '@speckle/shared' import { ensureError, Optional } from '@speckle/shared'
import { ServerInviteRecord } from '@/modules/serverinvites/domain/types' import { ServerInviteRecord } from '@/modules/serverinvites/domain/types'
import { import {
@@ -200,4 +200,4 @@ const githubStrategyBuilderFactory =
return strategy return strategy
} }
export = githubStrategyBuilderFactory export default githubStrategyBuilderFactory
@@ -212,4 +212,4 @@ const googleStrategyBuilderFactory =
return strategy return strategy
} }
export = googleStrategyBuilderFactory export default googleStrategyBuilderFactory
@@ -162,4 +162,4 @@ const localStrategyBuilderFactory =
return strategy return strategy
} }
export = localStrategyBuilderFactory export default localStrategyBuilderFactory
@@ -16,7 +16,7 @@ import { getNameFromUserInfo } from '@/modules/auth/helpers/oidc'
import { ServerInviteResourceType } from '@/modules/serverinvites/domain/constants' import { ServerInviteResourceType } from '@/modules/serverinvites/domain/constants'
import { getResourceTypeRole } from '@/modules/serverinvites/helpers/core' import { getResourceTypeRole } from '@/modules/serverinvites/helpers/core'
import { AuthStrategyBuilder } from '@/modules/auth/helpers/types' import { AuthStrategyBuilder } from '@/modules/auth/helpers/types'
import { get } from 'lodash' import { get } from 'lodash-es'
import { ensureError, Optional } from '@speckle/shared' import { ensureError, Optional } from '@speckle/shared'
import { ServerInviteRecord } from '@/modules/serverinvites/domain/types' import { ServerInviteRecord } from '@/modules/serverinvites/domain/types'
import { import {
@@ -203,4 +203,4 @@ const oidcStrategyBuilderFactory =
} }
} }
export = oidcStrategyBuilderFactory export default oidcStrategyBuilderFactory
@@ -62,7 +62,7 @@ import { RateLimiterMemory } from 'rate-limiter-flexible'
import { TIME } from '@speckle/shared' import { TIME } from '@speckle/shared'
import type { Application } from 'express' import type { Application } from 'express'
import { passportAuthenticationCallbackFactory } from '@/modules/auth/services/passportService' import { passportAuthenticationCallbackFactory } from '@/modules/auth/services/passportService'
import { testLogger as logger } from '@/observability/logging' import { extendLoggerComponent, logger as baseLogger } from '@/observability/logging'
import { import {
processFinalizedProjectInviteFactory, processFinalizedProjectInviteFactory,
validateProjectInviteBeforeFinalizationFactory validateProjectInviteBeforeFinalizationFactory
@@ -188,6 +188,7 @@ const createUser = createUserFactory({
}) })
const getUserByEmail = legacyGetUserByEmailFactory({ db }) const getUserByEmail = legacyGetUserByEmailFactory({ db })
const updateServerInfo = updateServerInfoFactory({ db }) const updateServerInfo = updateServerInfoFactory({ db })
const logger = extendLoggerComponent(baseLogger, 'auth-tests')
const expect = chai.expect const expect = chai.expect
@@ -2,7 +2,7 @@ import { faker } from '@faker-js/faker'
import { RelativeURL } from '@speckle/shared' import { RelativeURL } from '@speckle/shared'
import { expect } from 'chai' import { expect } from 'chai'
import type { Express } from 'express' import type { Express } from 'express'
import { has, isString } from 'lodash' import { has, isString } from 'lodash-es'
import request from 'supertest' import request from 'supertest'
export const appId = 'spklwebapp' // same values as on FE export const appId = 'spklwebapp' // same values as on FE
@@ -25,7 +25,6 @@ import {
TestApolloServer TestApolloServer
} from '@/test/graphqlHelper' } from '@/test/graphqlHelper'
import { beforeEachContext } from '@/test/hooks' import { beforeEachContext } from '@/test/hooks'
import { EmailSendingServiceMock } from '@/test/mocks/global'
import { captureCreatedInvite } from '@/test/speckle-helpers/inviteHelper' import { captureCreatedInvite } from '@/test/speckle-helpers/inviteHelper'
import { import {
BasicTestStream, BasicTestStream,
@@ -89,10 +88,6 @@ describe('Server registration', () => {
}) })
}) })
afterEach(() => {
EmailSendingServiceMock.resetMockedFunctions()
})
describe('with local strategy (email/pw)', () => { describe('with local strategy (email/pw)', () => {
it('works', async () => { it('works', async () => {
const challenge = 'asd123' const challenge = 'asd123'
@@ -30,7 +30,7 @@ import {
} from '@speckle/shared' } from '@speckle/shared'
import { randomUUID } from 'crypto' import { randomUUID } from 'crypto'
import { automateLogger, type Logger } from '@/observability/logging' 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' import { getRequestLogger } from '@/observability/utils/requestContext'
export type AuthCodePayloadWithOrigin = AuthCodePayload & { origin: string } export type AuthCodePayloadWithOrigin = AuthCodePayload & { origin: string }
@@ -17,7 +17,7 @@ import { getFeatureFlags } from '@/modules/shared/helpers/envHelper'
import { faker } from '@faker-js/faker' import { faker } from '@faker-js/faker'
import { Automate, isNullOrUndefined, SourceAppNames } from '@speckle/shared' import { Automate, isNullOrUndefined, SourceAppNames } from '@speckle/shared'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { times } from 'lodash' import { times } from 'lodash-es'
const { FF_AUTOMATE_MODULE_ENABLED } = getFeatureFlags() const { FF_AUTOMATE_MODULE_ENABLED } = getFeatureFlags()
@@ -94,7 +94,7 @@ import {
getFunctionInputsForFrontendFactory getFunctionInputsForFrontendFactory
} from '@/modules/automate/services/encryption' } from '@/modules/automate/services/encryption'
import { buildDecryptor } from '@/modules/shared/utils/libsodium' import { buildDecryptor } from '@/modules/shared/utils/libsodium'
import { keyBy } from 'lodash' import * as _ from 'lodash-es'
import { redactWriteOnlyInputData } from '@/modules/automate/utils/jsonSchemaRedactor' import { redactWriteOnlyInputData } from '@/modules/automate/utils/jsonSchemaRedactor'
import { import {
ProjectSubscriptions, ProjectSubscriptions,
@@ -142,7 +142,7 @@ const createAppToken = createAppTokenFactory({
storeUserServerAppToken: storeUserServerAppTokenFactory({ db }) storeUserServerAppToken: storeUserServerAppTokenFactory({ db })
}) })
export = (FF_AUTOMATE_MODULE_ENABLED export default (FF_AUTOMATE_MODULE_ENABLED
? { ? {
/** /**
* If automate module is enabled * If automate module is enabled
@@ -443,7 +443,7 @@ export = (FF_AUTOMATE_MODULE_ENABLED
const fns = await ctx.loaders const fns = await ctx.loaders
.forRegion({ db: projectDb }) .forRegion({ db: projectDb })
.automations.getRevisionFunctions.load(parent.id) .automations.getRevisionFunctions.load(parent.id)
const fnsReleases = keyBy( const fnsReleases = _.keyBy(
( (
await ctx.loaders await ctx.loaders
.forRegion({ db: projectDb }) .forRegion({ db: projectDb })
+1 -1
View File
@@ -394,4 +394,4 @@ const automateModule: SpeckleModule = {
} }
} }
export = automateModule export default automateModule
@@ -83,7 +83,7 @@ import {
import { Nullable, StreamRoles, isNullOrUndefined } from '@speckle/shared' import { Nullable, StreamRoles, isNullOrUndefined } from '@speckle/shared'
import cryptoRandomString from 'crypto-random-string' import cryptoRandomString from 'crypto-random-string'
import { Knex } from 'knex' 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' import { SetOptional, SetRequired } from 'type-fest'
const tables = { const tables = {
@@ -163,7 +163,7 @@ export const upsertAutomationFunctionRunFactory =
await tables await tables
.automationFunctionRuns(deps.db) .automationFunctionRuns(deps.db)
.insert( .insert(
_.pick(automationFunctionRun, AutomationFunctionRuns.withoutTablePrefix.cols) pick(automationFunctionRun, AutomationFunctionRuns.withoutTablePrefix.cols)
) )
.onConflict(AutomationFunctionRuns.withoutTablePrefix.col.id) .onConflict(AutomationFunctionRuns.withoutTablePrefix.col.id)
.merge([ .merge([
@@ -185,7 +185,7 @@ export const upsertAutomationRunFactory =
async (automationRun: InsertableAutomationRun) => { async (automationRun: InsertableAutomationRun) => {
await tables await tables
.automationRuns(deps.db) .automationRuns(deps.db)
.insert(_.pick(automationRun, AutomationRuns.withoutTablePrefix.cols)) .insert(pick(automationRun, AutomationRuns.withoutTablePrefix.cols))
.onConflict(AutomationRuns.withoutTablePrefix.col.id) .onConflict(AutomationRuns.withoutTablePrefix.col.id)
.merge([ .merge([
AutomationRuns.withoutTablePrefix.col.status, AutomationRuns.withoutTablePrefix.col.status,
@@ -198,7 +198,7 @@ export const upsertAutomationRunFactory =
.insert( .insert(
automationRun.triggers.map((t) => ({ automationRun.triggers.map((t) => ({
automationRunId: automationRun.id, automationRunId: automationRun.id,
..._.pick(t, AutomationRunTriggers.withoutTablePrefix.cols) ...pick(t, AutomationRunTriggers.withoutTablePrefix.cols)
})) }))
) )
.onConflict() .onConflict()
@@ -207,7 +207,7 @@ export const upsertAutomationRunFactory =
.automationFunctionRuns(deps.db) .automationFunctionRuns(deps.db)
.insert( .insert(
automationRun.functionRuns.map((f) => ({ automationRun.functionRuns.map((f) => ({
..._.pick(f, AutomationFunctionRuns.withoutTablePrefix.cols), ...pick(f, AutomationFunctionRuns.withoutTablePrefix.cols),
runId: automationRun.id runId: automationRun.id
})) }))
) )
@@ -372,7 +372,7 @@ export const storeAutomationRevisionFactory =
(deps: { db: Knex }): StoreAutomationRevision => (deps: { db: Knex }): StoreAutomationRevision =>
async (revision: InsertableAutomationRevision) => { async (revision: InsertableAutomationRevision) => {
const id = revision.id || generateRevisionId() const id = revision.id || generateRevisionId()
const rev = _.pick(revision, AutomationRevisions.withoutTablePrefix.cols) const rev = pick(revision, AutomationRevisions.withoutTablePrefix.cols)
const [newRev] = await tables const [newRev] = await tables
.automationRevisions(deps.db) .automationRevisions(deps.db)
.insert({ .insert({
@@ -4,7 +4,7 @@ import { AutomateAuthCodeHandshakeError } from '@/modules/automate/errors/manage
import { EventBus } from '@/modules/shared/services/eventBus' import { EventBus } from '@/modules/shared/services/eventBus'
import cryptoRandomString from 'crypto-random-string' import cryptoRandomString from 'crypto-random-string'
import Redis from 'ioredis' import Redis from 'ioredis'
import { get, has, isObjectLike } from 'lodash' import { get, has, isObjectLike } from 'lodash-es'
import { Logger } from 'pino' import { Logger } from 'pino'
import { WorkspaceEvents } from '@/modules/workspacesCore/domain/events' import { WorkspaceEvents } from '@/modules/workspacesCore/domain/events'
@@ -34,7 +34,7 @@ import {
AutomationRunStatuses, AutomationRunStatuses,
VersionCreationTriggerType VersionCreationTriggerType
} from '@/modules/automate/helpers/types' } from '@/modules/automate/helpers/types'
import { keyBy, uniq } from 'lodash' import { keyBy, uniq } from 'lodash-es'
import { resolveStatusFromFunctionRunStatuses } from '@/modules/automate/services/runsManagement' import { resolveStatusFromFunctionRunStatuses } from '@/modules/automate/services/runsManagement'
import { TriggeredAutomationsStatusGraphQLReturn } from '@/modules/automate/helpers/graphTypes' import { TriggeredAutomationsStatusGraphQLReturn } from '@/modules/automate/helpers/graphTypes'
import { FunctionInputDecryptor } from '@/modules/automate/services/encryption' 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 { ValidateStreamAccess } from '@/modules/core/domain/streams/operations'
import { EventBusEmit } from '@/modules/shared/services/eventBus' import { EventBusEmit } from '@/modules/shared/services/eventBus'
import { AutomationEvents } from '@/modules/automate/domain/events' 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 = { export type CreateAutomationDeps = {
createAuthCode: CreateStoredAuthCode createAuthCode: CreateStoredAuthCode
@@ -2,7 +2,7 @@ import { getEncryptionKeysPath } from '@/modules/shared/helpers/envHelper'
import { packageRoot } from '@/bootstrap' import { packageRoot } from '@/bootstrap'
import path from 'node:path' import path from 'node:path'
import fs from 'node:fs/promises' 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 { Nullable, Optional } from '@speckle/shared'
import { MisconfiguredEnvironmentError } from '@/modules/shared/errors' import { MisconfiguredEnvironmentError } from '@/modules/shared/errors'
import { AutomationFunctionInputEncryptionError } from '@/modules/automate/errors/management' import { AutomationFunctionInputEncryptionError } from '@/modules/automate/errors/management'
@@ -46,7 +46,7 @@ import { getFunctionsMarketplaceUrl } from '@/modules/core/helpers/routeHelper'
import type { Logger } from '@/observability/logging' import type { Logger } from '@/observability/logging'
import { CreateStoredAuthCode } from '@/modules/automate/domain/operations' import { CreateStoredAuthCode } from '@/modules/automate/domain/operations'
import { GetUser } from '@/modules/core/domain/users/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 { UnknownFunctionTemplateError } from '@/modules/automate/errors/functions'
import { UserInputError } from '@/modules/core/errors/userinput' import { UserInputError } from '@/modules/core/errors/userinput'
@@ -40,7 +40,7 @@ import {
import { BasicTestStream, createTestStreams } from '@/test/speckle-helpers/streamHelper' import { BasicTestStream, createTestStreams } from '@/test/speckle-helpers/streamHelper'
import { Automate, Roles } from '@speckle/shared' import { Automate, Roles } from '@speckle/shared'
import { expect } from 'chai' import { expect } from 'chai'
import { times } from 'lodash' import { times } from 'lodash-es'
import { getFeatureFlags } from '@/modules/shared/helpers/envHelper' import { getFeatureFlags } from '@/modules/shared/helpers/envHelper'
import { db } from '@/db/knex' import { db } from '@/db/knex'
import { import {
@@ -7,4 +7,4 @@ const backgroundJobsModule: SpeckleModule = {
} }
} }
export = backgroundJobsModule export default backgroundJobsModule
@@ -19,22 +19,26 @@ import {
GetSignedUrl GetSignedUrl
} from '@/modules/blobstorage/domain/operations' } from '@/modules/blobstorage/domain/operations'
export type ObjectStorage = {
client: S3Client
bucket: string
}
export type GetProjectObjectStorage = (args: { export type GetProjectObjectStorage = (args: {
projectId: string projectId: string
}) => Promise<ObjectStorage> }) => Promise<ObjectStorage>
export type GetObjectStorageParams = { export type GetObjectStorageParams = {
credentials: S3ClientConfig['credentials'] credentials: {
endpoint: S3ClientConfig['endpoint'] accessKeyId: string
region: S3ClientConfig['region'] secretAccessKey: string
}
endpoint: string
region: string
bucket: string bucket: string
} }
export type ObjectStorage = {
client: S3Client
bucket: string
params: GetObjectStorageParams
}
/** /**
* Get object storage client * Get object storage client
*/ */
@@ -48,7 +52,7 @@ export const getObjectStorage = (params: GetObjectStorageParams): ObjectStorage
forcePathStyle: true forcePathStyle: true
} }
const client = new S3Client(config) const client = new S3Client(config)
return { client, bucket } return { client, bucket, params }
} }
let mainObjectStorage: Optional<ObjectStorage> = undefined let mainObjectStorage: Optional<ObjectStorage> = undefined
@@ -65,7 +65,7 @@ const streamBlobResolvers = {
} }
} }
export = { export default {
ServerInfo: { ServerInfo: {
//deprecated //deprecated
blobSizeLimitBytes() { blobSizeLimitBytes() {
@@ -6,7 +6,7 @@ const TABLE_NAME = 'blob_storage'
* @param { import("knex").Knex } knex * @param { import("knex").Knex } knex
* @returns { Promise<void> } * @returns { Promise<void> }
*/ */
exports.up = async (knex) => { const up = async (knex) => {
await knex.schema.createTable(TABLE_NAME, (table) => { await knex.schema.createTable(TABLE_NAME, (table) => {
table.string('id', 10) table.string('id', 10)
// dont cascade on delete, cause it doesn't clean the object storage for the objs // 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) await knex.schema.dropTableIfExists(TABLE_NAME)
} }
export { up, down }
@@ -7,14 +7,16 @@ const HASH_COLUMN_NAME = 'fileHash'
* @param { import("knex").Knex } knex * @param { import("knex").Knex } knex
* @returns { Promise<void> } * @returns { Promise<void> }
*/ */
exports.up = async (knex) => { const up = async (knex) => {
await knex.schema.table(TABLE_NAME, (table) => { await knex.schema.table(TABLE_NAME, (table) => {
table.string(HASH_COLUMN_NAME) table.string(HASH_COLUMN_NAME)
}) })
} }
exports.down = async (knex) => { const down = async (knex) => {
await knex.schema.alterTable(TABLE_NAME, (table) => { await knex.schema.alterTable(TABLE_NAME, (table) => {
table.dropColumn(HASH_COLUMN_NAME) table.dropColumn(HASH_COLUMN_NAME)
}) })
} }
export { up, down }
@@ -2,7 +2,7 @@
* @param { import("knex").Knex } knex * @param { import("knex").Knex } knex
* @returns { Promise<void> } * @returns { Promise<void> }
*/ */
exports.up = async (knex) => { const up = async (knex) => {
await knex.raw( await knex.raw(
'ALTER TABLE "blob_storage" ALTER COLUMN "id" SET DATA TYPE varchar(255);' '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 * @param { import("knex").Knex } knex
* @returns { Promise<void> } * @returns { Promise<void> }
*/ */
exports.down = async (knex) => { const down = async (knex) => {
await knex.raw( await knex.raw(
'ALTER TABLE "blob_storage" ALTER COLUMN "id" SET DATA TYPE varchar(10);' 'ALTER TABLE "blob_storage" ALTER COLUMN "id" SET DATA TYPE varchar(10);'
) )
} }
export { up, down }
@@ -22,7 +22,7 @@ import {
import { Upload } from '@aws-sdk/lib-storage' import { Upload } from '@aws-sdk/lib-storage'
import type { Command } from '@aws-sdk/smithy-client' import type { Command } from '@aws-sdk/smithy-client'
import { ensureError } from '@speckle/shared' import { ensureError } from '@speckle/shared'
import { get } from 'lodash' import { get } from 'lodash-es'
import type stream from 'stream' import type stream from 'stream'
const sendCommand = async <CommandOutput extends ServiceOutputTypes>( const sendCommand = async <CommandOutput extends ServiceOutputTypes>(
@@ -6,7 +6,7 @@ import {
streamReadPermissionsPipelineFactory streamReadPermissionsPipelineFactory
} from '@/modules/shared/authz' } from '@/modules/shared/authz'
import { authMiddlewareCreator } from '@/modules/shared/middleware' import { authMiddlewareCreator } from '@/modules/shared/middleware'
import { isArray } from 'lodash' import { isArray } from 'lodash-es'
import { UnauthorizedError } from '@/modules/shared/errors' import { UnauthorizedError } from '@/modules/shared/errors'
import { import {
getAllStreamBlobIdsFactory, getAllStreamBlobIdsFactory,
@@ -16,7 +16,7 @@ import {
AlreadyRegisteredBlobError, AlreadyRegisteredBlobError,
StoredBlobAccessError StoredBlobAccessError
} from '@/modules/blobstorage/errors' } from '@/modules/blobstorage/errors'
import { isEmpty } from 'lodash' import { isEmpty } from 'lodash-es'
import { MisconfiguredEnvironmentError } from '@/modules/shared/errors' import { MisconfiguredEnvironmentError } from '@/modules/shared/errors'
// import { acceptedFileExtensions } from '@speckle/shared' // import { acceptedFileExtensions } from '@speckle/shared'
@@ -20,7 +20,7 @@ import { getProjectObjectStorage } from '@/modules/multiregion/utils/blobStorage
import { getProjectDbClient } from '@/modules/multiregion/utils/dbSelector' import { getProjectDbClient } from '@/modules/multiregion/utils/dbSelector'
import type { Logger } from '@/observability/logging' import type { Logger } from '@/observability/logging'
import type { Readable, Writable } from 'stream' 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 { UploadResult, ProcessingResult } from '@/modules/blobstorage/domain/types'
import type { Busboy } from 'busboy' import type { Busboy } from 'busboy'
@@ -42,6 +42,7 @@ import { BasicTestUser } from '@/test/authHelper'
import cryptoRandomString from 'crypto-random-string' import cryptoRandomString from 'crypto-random-string'
import type { BlobStorageItem } from '@/modules/blobstorage/domain/types' import type { BlobStorageItem } from '@/modules/blobstorage/domain/types'
import { getEventBus } from '@/modules/shared/services/eventBus' import { getEventBus } from '@/modules/shared/services/eventBus'
import { fileURLToPath } from 'url'
const getServerInfo = getServerInfoFactory({ db }) const getServerInfo = getServerInfoFactory({ db })
@@ -134,8 +135,8 @@ describe('Blobs integration @blobstorage', () => {
const response = await request(app) const response = await request(app)
.post(`/api/stream/${streamId}/blob`) .post(`/api/stream/${streamId}/blob`)
.set('Authorization', `Bearer ${token}`) .set('Authorization', `Bearer ${token}`)
.attach('blob1', require.resolve('@/readme.md')) .attach('blob1', fileURLToPath(import.meta.resolve('@/readme.md')))
.attach('blob2', require.resolve('@/package.json')) .attach('blob2', fileURLToPath(import.meta.resolve('@/package.json')))
expect(response.status).to.equal(201) expect(response.status).to.equal(201)
expect(response.body.uploadResults).to.exist expect(response.body.uploadResults).to.exist
const uploadResults = response.body.uploadResults const uploadResults = response.body.uploadResults
@@ -1,6 +1,6 @@
/* istanbul ignore file */ /* istanbul ignore file */
import crs from 'crypto-random-string' import crs from 'crypto-random-string'
import { range } from 'lodash' import { range } from 'lodash-es'
import { knex } from '@/db/knex' import { knex } from '@/db/knex'
const BlobStorage = () => knex('blob_storage') const BlobStorage = () => knex('blob_storage')
@@ -1,6 +1,6 @@
import { beforeEachContext } from '@/test/hooks' import { beforeEachContext } from '@/test/hooks'
import { NotFoundError, BadRequestError } from '@/modules/shared/errors' 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 { fakeIdGenerator, createBlobs } from '@/modules/blobstorage/tests/helpers'
import { import {
uploadFileStreamFactory, uploadFileStreamFactory,
@@ -25,7 +25,7 @@ import { Knex } from 'knex'
import cryptoRandomString from 'crypto-random-string' import cryptoRandomString from 'crypto-random-string'
import { expect } from 'chai' import { expect } from 'chai'
import { testLogger } from '@/observability/logging' import { testLogger } from '@/observability/logging'
import { put } from 'axios' import axios from 'axios'
import { expectToThrow } from '@/test/assertionHelper' import { expectToThrow } from '@/test/assertionHelper'
import { import {
AlreadyRegisteredBlobError, AlreadyRegisteredBlobError,
@@ -148,7 +148,7 @@ const { FF_LARGE_FILE_IMPORTS_ENABLED } = getFeatureFlags()
const fileSize = 100 const fileSize = 100
const response = await put(url, cryptoRandomString({ length: fileSize })) const response = await axios.put(url, cryptoRandomString({ length: fileSize }))
expect( expect(
response.status, response.status,
JSON.stringify({ statusText: response.statusText, body: response.data }) JSON.stringify({ statusText: response.statusText, body: response.data })
@@ -205,7 +205,7 @@ const { FF_LARGE_FILE_IMPORTS_ENABLED } = getFeatureFlags()
urlExpiryDurationSeconds: expiryDuration 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( expect(
response.status, response.status,
JSON.stringify({ statusText: response.statusText, body: response.data }) JSON.stringify({ statusText: response.statusText, body: response.data })
@@ -245,7 +245,7 @@ const { FF_LARGE_FILE_IMPORTS_ENABLED } = getFeatureFlags()
urlExpiryDurationSeconds: expiryDuration urlExpiryDurationSeconds: expiryDuration
}) })
const response = await put(url, 'test content') const response = await axios.put(url, 'test content')
expect( expect(
response.status, response.status,
JSON.stringify({ statusText: response.statusText, body: response.data }) JSON.stringify({ statusText: response.statusText, body: response.data })
@@ -291,7 +291,7 @@ const { FF_LARGE_FILE_IMPORTS_ENABLED } = getFeatureFlags()
urlExpiryDurationSeconds: expiryDuration 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( expect(
response.status, response.status,
JSON.stringify({ statusText: response.statusText, body: response.data }) JSON.stringify({ statusText: response.statusText, body: response.data })
@@ -337,7 +337,7 @@ const { FF_LARGE_FILE_IMPORTS_ENABLED } = getFeatureFlags()
const fileSize = 100 const fileSize = 100
const response = await put(url, cryptoRandomString({ length: fileSize })) const response = await axios.put(url, cryptoRandomString({ length: fileSize }))
expect( expect(
response.status, response.status,
JSON.stringify({ statusText: response.statusText, body: response.data }) JSON.stringify({ statusText: response.statusText, body: response.data })
@@ -1,4 +1,4 @@
import { noop } from 'lodash' import { noop } from 'lodash-es'
import { CommandModule } from 'yargs' import { CommandModule } from 'yargs'
const command: CommandModule = { const command: CommandModule = {
+1 -1
View File
@@ -1,4 +1,4 @@
import { noop } from 'lodash' import { noop } from 'lodash-es'
import { CommandModule } from 'yargs' import { CommandModule } from 'yargs'
const command: CommandModule = { const command: CommandModule = {
@@ -7,7 +7,7 @@ import {
NOTIFICATIONS_QUEUE, NOTIFICATIONS_QUEUE,
buildNotificationsQueue buildNotificationsQueue
} from '@/modules/notifications/services/queue' } from '@/modules/notifications/services/queue'
import { noop } from 'lodash' import { noop } from 'lodash-es'
import { cliLogger } from '@/observability/logging' import { cliLogger } from '@/observability/logging'
const PORT = 3032 const PORT = 3032
@@ -2,7 +2,7 @@ import { cliLogger } from '@/observability/logging'
import { NotificationType } from '@/modules/notifications/helpers/types' import { NotificationType } from '@/modules/notifications/helpers/types'
import { initializeConsumption } from '@/modules/notifications/index' import { initializeConsumption } from '@/modules/notifications/index'
import { EnvironmentResourceError } from '@/modules/shared/errors' import { EnvironmentResourceError } from '@/modules/shared/errors'
import { get, noop } from 'lodash' import { get, noop } from 'lodash-es'
import { CommandModule } from 'yargs' import { CommandModule } from 'yargs'
const command: CommandModule = { const command: CommandModule = {
+1 -1
View File
@@ -1,4 +1,4 @@
import { noop } from 'lodash' import { noop } from 'lodash-es'
import { CommandModule } from 'yargs' import { CommandModule } from 'yargs'
const command: CommandModule = { const command: CommandModule = {
@@ -1,4 +1,4 @@
import { noop } from 'lodash' import { noop } from 'lodash-es'
import { CommandModule } from 'yargs' import { CommandModule } from 'yargs'
const command: CommandModule = { const command: CommandModule = {
@@ -1,4 +1,4 @@
import { noop } from 'lodash' import { noop } from 'lodash-es'
import { CommandModule } from 'yargs' import { CommandModule } from 'yargs'
const command: CommandModule = { const command: CommandModule = {
@@ -7,7 +7,7 @@ import { getUserFactory } from '@/modules/core/repositories/users'
import { ForbiddenError } from '@/modules/shared/errors' import { ForbiddenError } from '@/modules/shared/errors'
import { BasicTestCommit, createTestCommits } from '@/test/speckle-helpers/commitHelper' import { BasicTestCommit, createTestCommits } from '@/test/speckle-helpers/commitHelper'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { times } from 'lodash' import { times } from 'lodash-es'
import { CommandModule } from 'yargs' import { CommandModule } from 'yargs'
import { ProjectRecordVisibility } from '@/modules/core/helpers/types' import { ProjectRecordVisibility } from '@/modules/core/helpers/types'
@@ -2,7 +2,7 @@ import { cliLogger as logger } from '@/observability/logging'
import { Users, ServerAcl } from '@/modules/core/dbSchema' import { Users, ServerAcl } from '@/modules/core/dbSchema'
import { Roles } from '@/modules/core/helpers/mainConstants' import { Roles } from '@/modules/core/helpers/mainConstants'
import { faker } from '@faker-js/faker' import { faker } from '@faker-js/faker'
import { range } from 'lodash' import { range } from 'lodash-es'
import { UniqueEnforcer } from 'enforce-unique' import { UniqueEnforcer } from 'enforce-unique'
import { CommandModule } from 'yargs' import { CommandModule } from 'yargs'
import { UserRecord } from '@/modules/core/helpers/types' import { UserRecord } from '@/modules/core/helpers/types'
@@ -1,4 +1,4 @@
import { noop } from 'lodash' import { noop } from 'lodash-es'
import { CommandModule } from 'yargs' import { CommandModule } from 'yargs'
const command: CommandModule = { const command: CommandModule = {
@@ -1,4 +1,4 @@
import { noop } from 'lodash' import { noop } from 'lodash-es'
import { CommandModule } from 'yargs' import { CommandModule } from 'yargs'
const command: CommandModule = { const command: CommandModule = {
@@ -18,7 +18,7 @@ const command: CommandModule<unknown, { file: string }> = {
}, },
handler: async ({ file }) => { handler: async ({ file }) => {
logger.info('Loading GQL schema...') logger.info('Loading GQL schema...')
const schema = ModulesSetup.graphSchema() const schema = await ModulesSetup.graphSchema()
const schemaString = printSchema(schema) const schemaString = printSchema(schema)
logger.info(`Saving to "${file}"...`) logger.info(`Saving to "${file}"...`)
@@ -1,4 +1,4 @@
import { noop } from 'lodash' import { noop } from 'lodash-es'
import { CommandModule } from 'yargs' import { CommandModule } from 'yargs'
const command: CommandModule = { const command: CommandModule = {
+1 -1
View File
@@ -1,4 +1,4 @@
import { noop } from 'lodash' import { noop } from 'lodash-es'
import { CommandModule } from 'yargs' import { CommandModule } from 'yargs'
const command: CommandModule = { const command: CommandModule = {
@@ -1,4 +1,4 @@
import { noop } from 'lodash' import { noop } from 'lodash-es'
import { CommandModule } from 'yargs' import { CommandModule } from 'yargs'
const command: CommandModule = { const command: CommandModule = {
+13 -11
View File
@@ -1,16 +1,20 @@
/* eslint-disable no-restricted-imports */ /* eslint-disable no-restricted-imports */
import '../../bootstrap.js'
import path from 'path' import path from 'path'
import yargs from 'yargs' import yargs from 'yargs'
import '../../bootstrap' import { hideBin } from 'yargs/helpers'
import { cliLogger as logger } from '@/observability/logging' import { cliLogger as logger } from '@/observability/logging'
import { isTestEnv } from '@/modules/shared/helpers/envHelper' 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 main = async () => {
const execution = yargs await yargs(hideBin(process.argv))
.scriptName('yarn cli') .scriptName('yarn cli')
.usage('$0 <cmd> [args]') .usage('$0 <cmd> [args]')
.commandDir(path.resolve(__dirname, './commands'), { extensions: ['js', 'ts'] }) .commandDir(path.resolve(getModuleDirectory(import.meta), './commands'), {
extensions: ['js', 'ts']
})
.option('beforeAll', { .option('beforeAll', {
type: 'boolean', type: 'boolean',
default: false, default: false,
@@ -24,7 +28,7 @@ const main = async () => {
// In test env, run beforeAll hooks to properly initialize everything first // In test env, run beforeAll hooks to properly initialize everything first
if (isBeforeAllSet && isTestEnv()) { if (isBeforeAllSet && isTestEnv()) {
logger.info('Running test beforeAll hooks...') logger.info('Running test beforeAll hooks...')
await (mochaHooks.beforeAll as () => Promise<void>)() await beforeEntireTestRun()
} }
}) })
.fail((msg, err, yargs) => { .fail((msg, err, yargs) => {
@@ -40,12 +44,10 @@ const main = async () => {
process.exit(1) process.exit(1)
}) })
.help().argv .help()
.parseAsync()
return execution process.exit(0)
} }
void main().then(() => { await main()
// weird TS typing issue
yargs.exit(0, undefined as unknown as Error)
})
@@ -1,5 +1,5 @@
import { defineRequestDataloaders } from '@/modules/shared/helpers/graphqlHelper' import { defineRequestDataloaders } from '@/modules/shared/helpers/graphqlHelper'
import { keyBy } from 'lodash' import { keyBy } from 'lodash-es'
import { Nullable } from '@/modules/shared/helpers/typeHelper' import { Nullable } from '@/modules/shared/helpers/typeHelper'
import { ResourceIdentifier } from '@/modules/core/graph/generated/graphql' import { ResourceIdentifier } from '@/modules/core/graph/generated/graphql'
import { import {
@@ -32,7 +32,7 @@ import {
ensureCommentSchema, ensureCommentSchema,
validateInputAttachmentsFactory validateInputAttachmentsFactory
} from '@/modules/comments/services/commentTextService' } from '@/modules/comments/services/commentTextService'
import { has } from 'lodash' import { has } from 'lodash-es'
import { import {
documentToBasicString, documentToBasicString,
SmartTextEditorValueSchema SmartTextEditorValueSchema
@@ -143,7 +143,7 @@ const getAuthorizedStreamCommentFactory =
return comment return comment
} }
export = { export default {
Query: { Query: {
async comment(_parent, args, context) { async comment(_parent, args, context) {
const projectId = args.streamId const projectId = args.streamId
+1 -1
View File
@@ -60,4 +60,4 @@ const commentsModule: SpeckleModule = {
} }
} }
export = commentsModule export default commentsModule
@@ -1,5 +1,5 @@
// /* istanbul ignore file */ // /* istanbul ignore file */
exports.up = async (knex) => { const up = async (knex) => {
await knex.schema.createTable('comments', (table) => { await knex.schema.createTable('comments', (table) => {
table.string('id', 10).primary() table.string('id', 10).primary()
table 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_views')
await knex.schema.dropTableIfExists('comment_links') await knex.schema.dropTableIfExists('comment_links')
await knex.schema.dropTableIfExists('comments') await knex.schema.dropTableIfExists('comments')
} }
export { up, down }
@@ -1,4 +1,4 @@
const { Users } = require('@/modules/core/dbSchema') import { Users } from '@/modules/core/dbSchema'
const COMMENTS_TABLE = 'comments' const COMMENTS_TABLE = 'comments'
const COMMENT_VIEWS_TABLE = 'comment_views' const COMMENT_VIEWS_TABLE = 'comment_views'
@@ -7,7 +7,7 @@ const COMMENT_VIEWS_TABLE = 'comment_views'
* @param { import("knex").Knex } knex * @param { import("knex").Knex } knex
* @returns { Promise<void> } * @returns { Promise<void> }
*/ */
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 // Delete all orphaned comments, which can be there even though there was a FK there before for some reason
await knex await knex
.table(COMMENTS_TABLE) .table(COMMENTS_TABLE)
@@ -41,7 +41,7 @@ exports.up = async function (knex) {
* @param { import("knex").Knex } knex * @param { import("knex").Knex } knex
* @returns { Promise<void> } * @returns { Promise<void> }
*/ */
exports.down = async function (knex) { async function down(knex) {
await knex.schema.alterTable(COMMENTS_TABLE, (table) => { await knex.schema.alterTable(COMMENTS_TABLE, (table) => {
table.dropForeign('authorId') table.dropForeign('authorId')
table.foreign('authorId').references(Users.col.id).onDelete('NO ACTION') 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') table.foreign('userId').references(Users.col.id).onDelete('NO ACTION')
}) })
} }
export { up, down }
@@ -20,7 +20,7 @@ import {
ResourceType ResourceType
} from '@/modules/core/graph/generated/graphql' } from '@/modules/core/graph/generated/graphql'
import { Optional } from '@/modules/shared/helpers/typeHelper' 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 crs from 'crypto-random-string'
import { executeBatchedSelect } from '@/modules/shared/helpers/dbHelper' import { executeBatchedSelect } from '@/modules/shared/helpers/dbHelper'
import { Knex } from 'knex' import { Knex } from 'knex'
@@ -8,7 +8,7 @@ import {
isDocEmpty, isDocEmpty,
documentToBasicString documentToBasicString
} from '@/modules/core/services/richTextEditorService' } from '@/modules/core/services/richTextEditorService'
import { isString, uniq } from 'lodash' import { isString, uniq } from 'lodash-es'
import { InvalidAttachmentsError } from '@/modules/comments/errors' import { InvalidAttachmentsError } from '@/modules/comments/errors'
import { JSONContent } from '@tiptap/core' import { JSONContent } from '@tiptap/core'
import { ValidateInputAttachments } from '@/modules/comments/domain/operations' import { ValidateInputAttachments } from '@/modules/comments/domain/operations'
@@ -5,7 +5,7 @@ import {
import { LegacyCommentViewerData } from '@/modules/core/graph/generated/graphql' import { LegacyCommentViewerData } from '@/modules/core/graph/generated/graphql'
import { viewerResourcesToString } from '@/modules/core/services/commit/viewerResources' import { viewerResourcesToString } from '@/modules/core/services/commit/viewerResources'
import { Nullable, SpeckleViewer } from '@speckle/shared' 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 type SerializedViewerState = SpeckleViewer.ViewerState.SerializedViewerState
@@ -62,7 +62,15 @@ export const createCommentFactory =
emitEvent: EventBusEmit emitEvent: EventBusEmit
getViewerResourcesFromLegacyIdentifiers: GetViewerResourcesFromLegacyIdentifiers 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) if (input.resources.length < 1)
throw new UserInputError( throw new UserInputError(
'Must specify at least one resource as the comment target' 'Must specify at least one resource as the comment target'
@@ -91,10 +99,12 @@ export const createCommentFactory =
} }
await deps.validateInputAttachments(input.streamId, input.blobIds) await deps.validateInputAttachments(input.streamId, input.blobIds)
comment.text = buildCommentTextFromInput({ comment.text = options?.skipTextValidation
doc: input.text, ? (input.text as SmartTextEditorValueSchema)
blobIds: input.blobIds : buildCommentTextFromInput({
}) doc: input.text,
blobIds: input.blobIds
})
const id = crs({ length: 10 }) const id = crs({ length: 10 })
const [newComment] = await deps.insertComments([ const [newComment] = await deps.insertComments([
@@ -2,7 +2,7 @@ import { CommentRecord } from '@/modules/comments/helpers/types'
import { ensureCommentSchema } from '@/modules/comments/services/commentTextService' import { ensureCommentSchema } from '@/modules/comments/services/commentTextService'
import type { JSONContent } from '@tiptap/core' import type { JSONContent } from '@tiptap/core'
import { iterateContentNodes } from '@/modules/core/services/richTextEditorService' import { iterateContentNodes } from '@/modules/core/services/richTextEditorService'
import { difference, flatten } from 'lodash' import { difference, flatten } from 'lodash-es'
import { import {
NotificationPublisher, NotificationPublisher,
NotificationType NotificationType
@@ -1,5 +1,5 @@
import { Optional } from '@speckle/shared' import { Optional } from '@speckle/shared'
import { isUndefined } from 'lodash' import { isUndefined } from 'lodash-es'
import { import {
GetPaginatedBranchCommentsFactory, GetPaginatedBranchCommentsFactory,
GetPaginatedBranchCommentsPage, GetPaginatedBranchCommentsPage,
@@ -11,13 +11,16 @@ import {
editCommentFactory, editCommentFactory,
archiveCommentFactory archiveCommentFactory
} from '@/modules/comments/services/index' } from '@/modules/comments/services/index'
import { convertBasicStringToDocument } from '@/modules/core/services/richTextEditorService' import {
convertBasicStringToDocument,
SmartTextEditorValueSchema
} from '@/modules/core/services/richTextEditorService'
import { import {
ensureCommentSchema, ensureCommentSchema,
buildCommentTextFromInput, buildCommentTextFromInput,
validateInputAttachmentsFactory validateInputAttachmentsFactory
} from '@/modules/comments/services/commentTextService' } from '@/modules/comments/services/commentTextService'
import { get, range } from 'lodash' import { get, range } from 'lodash-es'
import { buildApolloServer } from '@/app' import { buildApolloServer } from '@/app'
import { AllScopes } from '@/modules/core/helpers/mainConstants' import { AllScopes } from '@/modules/core/helpers/mainConstants'
import { createAuthTokenForUser } from '@/test/authHelper' import { createAuthTokenForUser } from '@/test/authHelper'
@@ -30,11 +33,6 @@ import {
purgeNotifications purgeNotifications
} from '@/test/notificationsHelper' } from '@/test/notificationsHelper'
import { NotificationType } from '@/modules/notifications/helpers/types' import { NotificationType } from '@/modules/notifications/helpers/types'
import {
EmailSendingServiceMock,
CommentsRepositoryMock,
StreamsRepositoryMock
} from '@/test/mocks/global'
import { createAuthedTestContext, ServerAndContext } from '@/test/graphqlHelper' import { createAuthedTestContext, ServerAndContext } from '@/test/graphqlHelper'
import { import {
checkStreamResourceAccessFactory, checkStreamResourceAccessFactory,
@@ -128,7 +126,6 @@ import {
LegacyCommentViewerData, LegacyCommentViewerData,
ReplyCreateInput ReplyCreateInput
} from '@/modules/core/graph/generated/graphql' } from '@/modules/core/graph/generated/graphql'
import { CommentRecord } from '@/modules/comments/helpers/types'
import { MaybeNullOrUndefined, TIME_MS } from '@speckle/shared' import { MaybeNullOrUndefined, TIME_MS } from '@speckle/shared'
import { CommentEvents } from '@/modules/comments/domain/events' import { CommentEvents } from '@/modules/comments/domain/events'
import { import {
@@ -136,7 +133,6 @@ import {
getViewerResourcesForCommentsFactory, getViewerResourcesForCommentsFactory,
getViewerResourcesFromLegacyIdentifiersFactory getViewerResourcesFromLegacyIdentifiersFactory
} from '@/modules/core/services/commit/viewerResources' } from '@/modules/core/services/commit/viewerResources'
import { StreamRecord } from '@/modules/core/helpers/types'
import { import {
processFinalizedProjectInviteFactory, processFinalizedProjectInviteFactory,
validateProjectInviteBeforeFinalizationFactory validateProjectInviteBeforeFinalizationFactory
@@ -146,11 +142,9 @@ import {
validateStreamAccessFactory validateStreamAccessFactory
} from '@/modules/core/services/streams/access' } from '@/modules/core/services/streams/access'
import { authorizeResolver } from '@/modules/shared' import { authorizeResolver } from '@/modules/shared'
import { createEmailListener, TestEmailListener } from '@/test/speckle-helpers/email'
type LegacyCommentRecord = CommentRecord & { import { buildTestProject } from '@/modules/core/tests/helpers/creation'
total_count: string import { GetCommentsQueryVariables } from '@/test/graphql/generated/graphql'
resources: Array<{ resourceId: string; resourceType: string }>
}
const getServerInfo = getServerInfoFactory({ db }) const getServerInfo = getServerInfoFactory({ db })
const getUser = getUserFactory({ db }) const getUser = getUserFactory({ db })
@@ -285,33 +279,34 @@ const buildFinalizeProjectInvite = () =>
getServerInfo getServerInfo
}) })
const createStream = legacyCreateStreamFactory({ const createStreamReturnRecord = createStreamReturnRecordFactory({
createStreamReturnRecord: createStreamReturnRecordFactory({ inviteUsersToProject: inviteUsersToProjectFactory({
inviteUsersToProject: inviteUsersToProjectFactory({ createAndSendInvite: createAndSendInviteFactory({
createAndSendInvite: createAndSendInviteFactory({ findUserByTarget: findUserByTargetFactory({ db }),
findUserByTarget: findUserByTargetFactory({ db }), insertInviteAndDeleteOld: insertInviteAndDeleteOldFactory({ db }),
insertInviteAndDeleteOld: insertInviteAndDeleteOldFactory({ db }), collectAndValidateResourceTargets: collectAndValidateCoreTargetsFactory({
collectAndValidateResourceTargets: collectAndValidateCoreTargetsFactory({ getStream
getStream
}),
buildInviteEmailContents: buildCoreInviteEmailContentsFactory({
getStream
}),
emitEvent: ({ eventName, payload }) =>
getEventBus().emit({
eventName,
payload
}),
getUser,
getServerInfo,
finalizeInvite: buildFinalizeProjectInvite()
}), }),
getUsers buildInviteEmailContents: buildCoreInviteEmailContentsFactory({
getStream
}),
emitEvent: ({ eventName, payload }) =>
getEventBus().emit({
eventName,
payload
}),
getUser,
getServerInfo,
finalizeInvite: buildFinalizeProjectInvite()
}), }),
createStream: createStreamFactory({ db }), getUsers
createBranch: createBranchFactory({ db }), }),
emitEvent: getEventBus().emit createStream: createStreamFactory({ db }),
}) createBranch: createBranchFactory({ db }),
emitEvent: getEventBus().emit
})
const createStream = legacyCreateStreamFactory({
createStreamReturnRecord
}) })
const findEmail = findEmailFactory({ db }) const findEmail = findEmailFactory({ db })
@@ -353,9 +348,8 @@ function generateRandomCommentText() {
return buildCommentInputFromString(crs({ length: 10 })) return buildCommentInputFromString(crs({ length: 10 }))
} }
const mailerMock = EmailSendingServiceMock const buildTestStream = () =>
const commentRepoMock = CommentsRepositoryMock buildTestProject({ workspaceId: undefined, regionKey: undefined })
const streamsRepoMock = StreamsRepositoryMock
describe('Comments @comments', () => { describe('Comments @comments', () => {
let app: express.Express let app: express.Express
@@ -434,13 +428,6 @@ describe('Comments @comments', () => {
after(() => { after(() => {
notificationsState.destroy() notificationsState.destroy()
commentRepoMock.destroy()
streamsRepoMock.destroy()
})
afterEach(() => {
commentRepoMock.disable()
commentRepoMock.resetMockedFunctions()
}) })
it('Should be able to create a comment and a reply', async () => { 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, { CommentsGraphQLClient.createComment(apollo, {
input: { input: {
streamId: stream.id, streamId: stream.id,
@@ -1509,7 +1496,7 @@ describe('Comments @comments', () => {
} }
}) })
const createReply = (input?: ReplyCreateInput) => const createReplyGql = (input?: ReplyCreateInput) =>
CommentsGraphQLClient.createReply(apollo, { CommentsGraphQLClient.createReply(apollo, {
input: { input: {
streamId: stream.id, streamId: stream.id,
@@ -1528,7 +1515,7 @@ describe('Comments @comments', () => {
await truncateTables([Comments.name]) await truncateTables([Comments.name])
// Create a single comment with a blob // Create a single comment with a blob
const createCommentResult = await createComment({ const createCommentResult = await createCommentGql({
text: generateRandomCommentText(), text: generateRandomCommentText(),
blobIds: [blob1.blobId] blobIds: [blob1.blobId]
}) })
@@ -1536,7 +1523,7 @@ describe('Comments @comments', () => {
if (!parentCommentId) throw new Error('Comment creation failed!') if (!parentCommentId) throw new Error('Comment creation failed!')
// Create a reply with a blob // Create a reply with a blob
await createReply({ await createReplyGql({
text: generateRandomCommentText(), text: generateRandomCommentText(),
blobIds: [blob1.blobId], blobIds: [blob1.blobId],
parentComment: parentCommentId, parentComment: parentCommentId,
@@ -1544,7 +1531,7 @@ describe('Comments @comments', () => {
}) })
// Create a reply with a blob, but no text // Create a reply with a blob, but no text
const emptyCommentResult = await createReply({ const emptyCommentResult = await createReplyGql({
blobIds: [blob1.blobId], blobIds: [blob1.blobId],
parentComment: parentCommentId, parentComment: parentCommentId,
streamId: stream.id streamId: stream.id
@@ -1559,7 +1546,7 @@ describe('Comments @comments', () => {
...(input || { id: '' }) ...(input || { id: '' })
}) })
const readComments = (input = {}) => const readComments = (input: Partial<GetCommentsQueryVariables> = {}) =>
CommentsGraphQLClient.getComments(apollo, { CommentsGraphQLClient.getComments(apollo, {
cursor: null, cursor: null,
streamId: stream.id, 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 () => { it('both legacy (string) comments and new (ProseMirror) documents are formatted as SmartTextEditorValue values', async () => {
commentRepoMock.enable() const streamId = await createStream({ ...buildTestStream(), ownerId: user.id })
commentRepoMock.mockFunction('getCommentsLegacyFactory', () => {
return async () => ({ await Promise.all([
items: [ // Legacy
// Legacy createComment(
{ {
id: 'a', userId: user.id,
text: 'hey dude! welcome to my legacy-type comment!', input: {
streamId: stream.id streamId,
}, resources: [
// New { resourceId: streamId, resourceType: ResourceType.Stream }
{ ],
id: 'b', 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( text: JSON.stringify(
buildCommentTextFromInput({ buildCommentTextFromInput({
doc: buildCommentInputFromString('new comment schema here') doc: buildCommentInputFromString('new comment schema here')
}) })
), ) as unknown as SmartTextEditorValueSchema,
streamId: stream.id data: {},
}, blobIds: []
// New, but for some reason the text object is already deserialized }
{ },
id: 'c', { 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({ text: buildCommentTextFromInput({
doc: buildCommentInputFromString('another new comment schema here') doc: buildCommentInputFromString('another new comment schema here')
}), }),
streamId: stream.id data: {},
blobIds: []
} }
] as unknown as Array<LegacyCommentRecord>, },
cursor: new Date().toISOString(), { skipTextValidation: true }
totalCount: 3 )
}) ])
})
const { data, errors } = await readComments() const { data, errors } = await readComments({
streamId
})
expect(errors?.length || 0).to.eq(0) expect(errors?.length || 0).to.eq(0)
expect(data?.comments?.items?.length || 0).to.eq(3) expect(data?.comments?.items?.length || 0).to.eq(3)
}) })
it('legacy comment with a single link is formatted correctly', async () => { 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 = { const item = {
id: '1', text: 'https://aaa.com:3000/h3ll0-world/_?a=1&b=2#aaa' as unknown as SmartTextEditorValueSchema,
text: 'https://aaa.com:3000/h3ll0-world/_?a=1&b=2#aaa', streamId,
streamId: stream.id authorId: user.id
} as unknown as LegacyCommentRecord }
await createComment(
{
userId: user.id,
input: {
streamId,
resources: [{ resourceId: streamId, resourceType: ResourceType.Stream }],
text: item.text,
data: {},
blobIds: []
}
},
{ skipTextValidation: true }
)
commentRepoMock.enable() const { data, errors } = await readComments({
commentRepoMock.mockFunction('getCommentsLegacyFactory', () => async () => ({ streamId
items: [item], })
cursor: new Date().toISOString(),
totalCount: 1
}))
const { data, errors } = await readComments()
expect(data?.comments?.items?.length || 0).to.eq(1) expect(data?.comments?.items?.length || 0).to.eq(1)
expect(errors?.length || 0).to.eq(0) expect(errors?.length || 0).to.eq(0)
@@ -1637,6 +1662,8 @@ describe('Comments @comments', () => {
}) })
it('legacy comment with multiple links formats them correctly', async () => { it('legacy comment with multiple links formats them correctly', async () => {
const streamId = await createStream({ ...buildTestStream(), ownerId: user.id })
const textParts = [ const textParts = [
"Here's one ", "Here's one ",
// The period and comma def shouldn't belong to the following URL, but we have a pretty basic // 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' 'http://agag.com:3000'
] ]
// Low-level insert cause all we need are just the main DB entries
const item = { const item = {
id: '1', text: textParts.join('') as unknown as SmartTextEditorValueSchema,
text: textParts.join(''), streamId,
streamId: stream.id authorId: user.id
} as unknown as LegacyCommentRecord }
await createComment(
commentRepoMock.enable() {
commentRepoMock.mockFunction('getCommentsLegacyFactory', () => async () => ({ userId: user.id,
items: [item], input: {
cursor: new Date().toISOString(), streamId,
totalCount: 1 resources: [{ resourceId: streamId, resourceType: ResourceType.Stream }],
})) text: item.text,
data: {},
const { data, errors } = await readComments() blobIds: []
}
},
{ skipTextValidation: true }
)
const { data, errors } = await readComments({ streamId })
const runExpectationsOnTextNode = (idx: number, shouldBeLink: boolean) => { const runExpectationsOnTextNode = (idx: number, shouldBeLink: boolean) => {
expect(textNodes[idx].text).to.eq(textParts[idx]) expect(textNodes[idx].text).to.eq(textParts[idx])
@@ -1724,7 +1757,7 @@ describe('Comments @comments', () => {
}) })
it('returns raw text correctly', async () => { it('returns raw text correctly', async () => {
const { data } = await createReply({ const { data } = await createReplyGql({
text: { text: {
type: 'doc', type: 'doc',
content: [ content: [
@@ -1761,43 +1794,6 @@ describe('Comments @comments', () => {
expect(data?.comment?.text?.doc).to.be.null expect(data?.comment?.text?.doc).to.be.null
expect(data?.comment?.text?.attachments?.length).to.be.greaterThan(0) 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 = [ const creatingOrReplyingDataSet = [
@@ -1809,8 +1805,8 @@ describe('Comments @comments', () => {
const createOrReplyComment = (input = {}) => const createOrReplyComment = (input = {}) =>
creating creating
? createComment(input) ? createCommentGql(input)
: createReply({ : createReplyGql({
parentComment: parentCommentId, parentComment: parentCommentId,
blobIds: [], blobIds: [],
streamId: stream.id, streamId: stream.id,
@@ -1825,7 +1821,7 @@ describe('Comments @comments', () => {
before(async () => { before(async () => {
if (replying) { if (replying) {
// Create comment for attaching replies to // Create comment for attaching replies to
const { data } = await createComment({ const { data } = await createCommentGql({
text: generateRandomCommentText() text: generateRandomCommentText()
}) })
@@ -1920,6 +1916,20 @@ describe('Comments @comments', () => {
}) })
describe('and mentioning a user', () => { 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 = {}) => const createOrReplyCommentWithMention = (targetUserId: string, input = {}) =>
createOrReplyComment({ createOrReplyComment({
text: { text: {
@@ -1942,10 +1952,7 @@ describe('Comments @comments', () => {
}) })
it('a valid mention triggers a notification', async () => { it('a valid mention triggers a notification', async () => {
const sendEmailInvocations = mailerMock.hijackFunction( const { getSends } = emailListener.listen({ times: 2 })
'sendEmail',
async () => false
)
const waitForAck = notificationsState.waitForAck( const waitForAck = notificationsState.waitForAck(
(e) => e.result?.type === NotificationType.MentionedInComment (e) => e.result?.type === NotificationType.MentionedInComment
@@ -1960,7 +1967,8 @@ describe('Comments @comments', () => {
// Wait for // Wait for
await waitForAck await waitForAck
const emailParams = sendEmailInvocations.args[0][0] const emailSends = getSends()
const emailParams = emailSends[0]
expect(emailParams).to.be.ok expect(emailParams).to.be.ok
expect(emailParams.subject).to.contain('mentioned in a Speckle comment') expect(emailParams.subject).to.contain('mentioned in a Speckle comment')
expect(emailParams.to).to.eq(otherUser.email) expect(emailParams.to).to.eq(otherUser.email)
+1 -1
View File
@@ -3,7 +3,7 @@ import { Optional } from '@speckle/shared'
import knex from '@/db/knex' import knex from '@/db/knex'
import { BaseMetaRecord } from '@/modules/core/helpers/meta' import { BaseMetaRecord } from '@/modules/core/helpers/meta'
import { Knex } from 'knex' import { Knex } from 'knex'
import { reduce } from 'lodash' import { reduce } from 'lodash-es'
type BaseInnerSchemaConfig<T extends string, C extends string> = { type BaseInnerSchemaConfig<T extends string, C extends string> = {
/** /**
@@ -24,7 +24,7 @@ import {
UserSubscriptions, UserSubscriptions,
WorkspaceSubscriptions WorkspaceSubscriptions
} from '@/modules/shared/utils/subscriptions' } from '@/modules/shared/utils/subscriptions'
import { chunk, flatten } from 'lodash' import { chunk, flatten } from 'lodash-es'
const reportModelCreatedFactory = const reportModelCreatedFactory =
(deps: { publish: PublishSubscription }) => (deps: { publish: PublishSubscription }) =>

Some files were not shown because too many files have changed in this diff Show More