+ Sent from <%= params.server.name || 'Speckle Server' %> at
+
+ <%= params.server.url %>
+
+ , deployed and managed by <%= params.server.company %>. Your
+ admin contact is <%= params.server.contact %>.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Brought to you by
+
+ Speckle
+
+ , the Open Source Data Platform for 3D Data
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/server/assets/emails/templates/basic/basic.txt b/packages/server/assets/emails/templates/basic/basic.txt
new file mode 100644
index 000000000..8eb4ab482
--- /dev/null
+++ b/packages/server/assets/emails/templates/basic/basic.txt
@@ -0,0 +1,6 @@
+<%_ if (params.text?.bodyStart?.length) print("\n" + params.text.bodyStart + "\n"); _%>
+<%_ if (params.cta) print("\n" + params.cta.title + ': ' + params.cta.url + "\n"); _%>
+<%_ if (params.text?.bodyEnd?.length) print("\n" + params.text.bodyEnd + "\n"); _%>
+
+------------------------------------------------------
+Sent from <%= params.server.name || 'Speckle Server' %> at <%= params.server.url %>, deployed and managed by <%= params.server.company %>. Your admin contact is <%= params.server.contact %>.
\ No newline at end of file
diff --git a/packages/server/bootstrap.js b/packages/server/bootstrap.js
index 68a0d3c3e..a8baa809d 100644
--- a/packages/server/bootstrap.js
+++ b/packages/server/bootstrap.js
@@ -3,17 +3,17 @@
* Bootstrap module that should be imported at the very top of each entry point module
*/
-// Conditionally change appRoot and repoRoot according to whether we're running from /dist/ or not (ts-node)
+// 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 repoRoot = isTsNode ? appRoot : path.resolve(__dirname, '../')
+const packageRoot = isTsNode ? appRoot : path.resolve(__dirname, '../')
// Initializing module aliases for absolute import paths
const moduleAlias = require('module-alias')
moduleAlias.addAliases({
'@': appRoot,
- '#': repoRoot
+ '#': packageRoot
})
// Initializing env vars
@@ -32,7 +32,7 @@ if (isApolloMonitoringEnabled() && !getApolloServerVersion()) {
// If running in test env, load .env.test first
// (appRoot necessary, cause env files aren't loaded through require() calls)
if (isTestEnv()) {
- const { error } = dotenv.config({ path: `${repoRoot}/.env.test` })
+ const { error } = dotenv.config({ path: `${packageRoot}/.env.test` })
if (error) {
const e = new Error(
'Attempting to run tests without an .env.test file properly set up! Check readme!'
@@ -42,9 +42,9 @@ if (isTestEnv()) {
}
}
-dotenv.config({ path: `${repoRoot}/.env` })
+dotenv.config({ path: `${packageRoot}/.env` })
module.exports = {
appRoot,
- repoRoot
+ packageRoot
}
diff --git a/packages/server/knexfile.js b/packages/server/knexfile.js
index f0eec3b91..c895e7d70 100644
--- a/packages/server/knexfile.js
+++ b/packages/server/knexfile.js
@@ -2,7 +2,7 @@
/* istanbul ignore file */
'use strict'
-const { repoRoot } = require('./bootstrap')
+const { packageRoot } = require('./bootstrap')
const fs = require('fs')
const path = require('path')
const { isTestEnv } = require('@/modules/shared/helpers/envHelper')
@@ -26,7 +26,7 @@ function walk(dir) {
// The only exception is when running tests in the test DB, cause the stakes are way lower there and we always
// run them through ts-node anyway, so it doesn't make sense forcing the app to be built
const migrationModulesDir = path.resolve(
- repoRoot,
+ packageRoot,
isTestEnv() ? './modules' : './dist/modules'
)
if (!fs.existsSync(migrationModulesDir)) {
diff --git a/packages/server/modules/comments/tests/comments.spec.js b/packages/server/modules/comments/tests/comments.spec.js
index dae4b13d6..939e7dc3a 100644
--- a/packages/server/modules/comments/tests/comments.spec.js
+++ b/packages/server/modules/comments/tests/comments.spec.js
@@ -6,7 +6,7 @@ const commentsServiceMock = mockRequireModule(
)
const path = require('path')
-const { repoRoot } = require('@/bootstrap')
+const { packageRoot } = require('@/bootstrap')
const expect = require('chai').expect
const crs = require('crypto-random-string')
const { beforeEachContext, truncateTables } = require('@/test/hooks')
@@ -1018,7 +1018,7 @@ describe('Comments @comments', () => {
// Upload a small blob
blob1 = await uploadBlob(
app,
- path.resolve(repoRoot, './test/assets/testimage1.jpg'),
+ path.resolve(packageRoot, './test/assets/testimage1.jpg'),
stream.id,
{
authToken: userToken
diff --git a/packages/server/modules/core/dbSchema.js b/packages/server/modules/core/dbSchema.js
deleted file mode 100644
index 422cb0f6e..000000000
--- a/packages/server/modules/core/dbSchema.js
+++ /dev/null
@@ -1,107 +0,0 @@
-const knex = require('@/db/knex')
-
-/**
- * Single source of truth for DB schema in the codebase
- */
-
-/**
- * TODO:
- * ServerInvites:
- * - Get rid of the 'used' field, it's not used anymore
- *
- * TODO: Redo this when we have TS support with nice typing, ability to get columns with/without aliases
- */
-
-module.exports = {
- Streams: {
- name: 'streams',
- knex: () => knex('streams'),
- col: {
- id: 'streams.id',
- name: 'streams.name',
- description: 'streams.description',
- isPublic: 'streams.isPublic',
- clonedFrom: 'streams.clonedFrom',
- createdAt: 'streams.createdAt',
- updatedAt: 'streams.updatedAt'
- }
- },
- StreamAcl: {
- name: 'stream_acl',
- knex: () => knex('stream_acl'),
- col: {
- userId: 'stream_acl.userId',
- resourceId: 'stream_acl.resourceId',
- role: 'stream_acl.role'
- }
- },
- StreamFavorites: {
- name: 'stream_favorites',
- knex: () => knex('stream_favorites'),
- col: {
- streamId: 'stream_favorites.streamId',
- userId: 'stream_favorites.userId',
- createdAt: 'stream_favorites.createdAt',
- cursor: 'stream_favorites.cursor'
- }
- },
- Users: {
- name: 'users',
- knex: () => knex('users'),
- col: {
- id: 'users.id',
- suuid: 'users.suuid',
- createdAt: 'users.createdAt',
- name: 'users.name',
- bio: 'users.bio',
- company: 'users.company',
- email: 'users.email',
- verified: 'users.verified',
- avatar: 'users.avatar',
- profiles: 'users.profiles',
- passwordDigest: 'users.passwordDigest',
- ip: 'users.ip'
- }
- },
- ServerAcl: {
- name: 'server_acl',
- knex: () => knex('server_acl'),
- col: {
- userId: 'server_acl.userId',
- role: 'server_acl.role'
- }
- },
- Comments: {
- name: 'comments',
- knex: () => knex('comments'),
- col: {
- id: 'comments.id',
- streamId: 'comments.streamId',
- authorId: 'comments.authorId',
- createdAt: 'comments.createdAt',
- updatedAt: 'comments.updatedAt',
- text: 'comments.text',
- screenshot: 'comments.screenshot',
- data: 'comments.data',
- archived: 'comments.archived',
- parentComment: 'comments.parentComment'
- }
- },
- ServerInvites: {
- name: 'server_invites',
- knex: () => knex('server_invites'),
- col: {
- id: 'server_invites.id',
- target: 'server_invites.target',
- inviterId: 'server_invites.inviterId',
- createdAt: 'server_invites.createdAt',
- used: 'server_invites.used',
- message: 'server_invites.message',
- resourceTarget: 'server_invites.resourceTarget',
- resourceId: 'server_invites.resourceId',
- role: 'server_invites.role',
- token: 'server_invites.token'
- }
- },
- knex
-}
diff --git a/packages/server/modules/core/dbSchema.ts b/packages/server/modules/core/dbSchema.ts
new file mode 100644
index 000000000..f0d998de1
--- /dev/null
+++ b/packages/server/modules/core/dbSchema.ts
@@ -0,0 +1,168 @@
+import knex from '@/db/knex'
+import { Knex } from 'knex'
+import { reduce } from 'lodash'
+
+/**
+ * TODO:
+ * ServerInvites:
+ * - Get rid of the 'used' field, it's not used anymore
+ */
+
+type SchemaConfig = InnerSchemaConfig & {
+ /**
+ * Return schema helper with custom configuration options
+ */
+ with: (params?: SchemaConfigParams) => InnerSchemaConfig
+}
+
+type InnerSchemaConfig = {
+ /**
+ * Table name
+ */
+ name: T
+ /**
+ * Get `knex(tableName)` QueryBuilder instance
+ */
+ knex: () => Knex.QueryBuilder
+ /**
+ * Get names of table columns. The names can be prefixed with the table name or not, depending
+ * on whether `withoutTablePrefix` was set when accessing the helper.
+ */
+ col: {
+ [colName in C]: string
+ }
+}
+
+type SchemaConfigParams = {
+ /**
+ * Configure `col` properties to not have the table name prefixed. For the most part you want the prefix,
+ * cause this helps in queries with JOINS (when multiple tables have a col with the same name), but you don't
+ * want the prefix when triggering UPDATE queries, because the `SET = ` syntax doesn't support
+ * column names with table prefixes.
+ */
+ withoutTablePrefix?: boolean
+}
+
+function buildTableHelper(
+ tableName: T,
+ columns: C[]
+): SchemaConfig {
+ function buildInnerSchemaConfig(
+ params: SchemaConfigParams = {}
+ ): InnerSchemaConfig {
+ return {
+ name: tableName,
+ knex: () => knex(tableName),
+ col: reduce(
+ columns,
+ (prev, curr) => {
+ prev[curr] = params.withoutTablePrefix ? curr : `${tableName}.${curr}`
+ return prev
+ },
+ {} as Record
+ )
+ }
+ }
+
+ return {
+ ...buildInnerSchemaConfig(),
+ with: buildInnerSchemaConfig
+ }
+}
+
+/*
+ * TABLE RECORD TYPES
+ */
+
+export type ServerInviteRecord = {
+ id: string
+ target: string
+ inviterId: string
+ createdAt?: Date
+ used?: boolean
+ message?: string
+ resourceTarget?: string
+ resourceId?: string
+ role?: string
+ token: string
+}
+
+/*
+ * TABLE HELPERS
+ * The generated helpers are used like this:
+ *
+ * Streams.name - TableName
+ * Streams.col.id - Get column names
+ * Streams.knex() - Get knex() instance for this specific table
+ *
+ * Streams.with({...}) - configure helper, e.g. disable table name being prefixed to col names:
+ * Streams.with({withoutTablePrefix: true}).col.id
+ */
+
+export const Streams = buildTableHelper('streams', [
+ 'id',
+ 'name',
+ 'description',
+ 'isPublic',
+ 'clonedFrom',
+ 'createdAt',
+ 'updatedAt'
+])
+
+export const StreamAcl = buildTableHelper('stream_acl', [
+ 'userId',
+ 'resourceId',
+ 'role'
+])
+
+export const StreamFavorites = buildTableHelper('stream_favorites', [
+ 'streamId',
+ 'userId',
+ 'createdAt',
+ 'cursor'
+])
+
+export const Users = buildTableHelper('users', [
+ 'id',
+ 'suuid',
+ 'createdAt',
+ 'name',
+ 'bio',
+ 'company',
+ 'email',
+ 'verified',
+ 'avatar',
+ 'profiles',
+ 'passwordDigest',
+ 'ip'
+])
+
+export const ServerAcl = buildTableHelper('server_acl', ['userId', 'role'])
+
+export const Comments = buildTableHelper('comments', [
+ 'id',
+ 'streamId',
+ 'authorId',
+ 'createdAt',
+ 'updatedAt',
+ 'text',
+ 'screenshot',
+ 'data',
+ 'archived',
+ 'parentComment'
+])
+
+export const ServerInvites = buildTableHelper('server_invites', [
+ 'id',
+ 'target',
+ 'inviterId',
+ 'createdAt',
+ 'used',
+ 'message',
+ 'resourceTarget',
+ 'resourceId',
+ 'role',
+ 'token'
+])
+
+export { knex }
diff --git a/packages/server/modules/core/tests/graphSubs.spec.js b/packages/server/modules/core/tests/graphSubs.spec.js
index 0a42afbdf..6d2b03d8a 100644
--- a/packages/server/modules/core/tests/graphSubs.spec.js
+++ b/packages/server/modules/core/tests/graphSubs.spec.js
@@ -13,7 +13,7 @@ const { createPersonalAccessToken } = require('../services/tokens')
const { beforeEachContext } = require(`@/test/hooks`)
const { sleep, noErrors } = require('@/test/helpers')
-const { repoRoot } = require('@/bootstrap')
+const { packageRoot } = require('@/bootstrap')
const {
addOrUpdateStreamCollaborator
} = require('@/modules/core/services/streams/streamAccessService')
@@ -70,7 +70,7 @@ describe('GraphQL API Subscriptions @gql-subscriptions', () => {
serverProcess = childProcess.spawn(
/^win/.test(process.platform) ? 'npm.cmd' : 'npm',
['run', 'dev:server:test'],
- { cwd: repoRoot.path }
+ { cwd: packageRoot }
)
const reg = /running at 0.0.0.0:([0-9]*)/
diff --git a/packages/server/modules/core/tests/usersAdminList.spec.ts b/packages/server/modules/core/tests/usersAdminList.spec.ts
index d6d3be917..0307ab777 100644
--- a/packages/server/modules/core/tests/usersAdminList.spec.ts
+++ b/packages/server/modules/core/tests/usersAdminList.spec.ts
@@ -1,4 +1,9 @@
-import { ServerInvites, Streams, Users } from '@/modules/core/dbSchema'
+import {
+ ServerInviteRecord,
+ ServerInvites,
+ Streams,
+ Users
+} from '@/modules/core/dbSchema'
import { truncateTables } from '@/test/hooks'
import { createUser } from '@/modules/core/services/users'
import { createStream } from '@/modules/core/services/streams'
@@ -25,11 +30,13 @@ async function getOrderedInviteIds() {
await ServerInvites.knex()
.select(ServerInvites.col.id)
.where(ServerInvites.col.target, 'NOT ILIKE', `@%`)
- ).map((o) => o.id)
+ ).map((o: Pick) => o.id)
}
async function getOrderedUserIds() {
- return (await Users.knex().select(Users.col.id)).map((o) => o.id)
+ return (await Users.knex().select(Users.col.id)).map(
+ (o: Pick) => o.id
+ )
}
describe('[Admin users list]', () => {
diff --git a/packages/server/modules/emails/index.js b/packages/server/modules/emails/index.js
deleted file mode 100644
index d99df4d14..000000000
--- a/packages/server/modules/emails/index.js
+++ /dev/null
@@ -1,71 +0,0 @@
-/* istanbul ignore file */
-'use strict'
-const debug = require('debug')('speckle')
-
-const nodemailer = require('nodemailer')
-const modulesDebug = debug.extend('modules')
-const errorDebug = debug.extend('errors')
-
-let transporter
-
-const createJsonEchoTransporter = () =>
- nodemailer.createTransport({
- jsonTransport: true
- })
-
-const initSmtpTransporter = async () => {
- try {
- const smtpTransporter = nodemailer.createTransport({
- host: process.env.EMAIL_HOST,
- port: process.env.EMAIL_PORT || 587,
- secure: process.env.EMAIL_SECURE === 'true',
- auth: {
- user: process.env.EMAIL_USERNAME,
- pass: process.env.EMAIL_PASSWORD
- }
- })
- await smtpTransporter.verify()
- return smtpTransporter
- } catch {
- errorDebug('📧 Email provider is misconfigured, check config variables.')
- }
-}
-
-const initTransporter = async () => {
- if (process.env.NODE_ENV === 'test') return createJsonEchoTransporter()
- if (process.env.EMAIL === 'true') return await initSmtpTransporter()
-
- modulesDebug(
- '📧 Email provider is not configured. Server functionality will be limited.'
- )
-}
-
-exports.init = async (app) => {
- modulesDebug('📧 Init emails module')
- transporter = await initTransporter()
- require('./rest')(app)
-}
-
-exports.finalize = async () => {
- // Nothing to do here.
-}
-
-exports.sendEmail = async ({ from, to, subject, text, html }) => {
- // note, the transporter is only initialized with the app init step
- if (!transporter) {
- errorDebug('No email transport present. Cannot send emails.')
- return false
- }
- try {
- const emailFrom = process.env.EMAIL_FROM || 'no-reply@speckle.systems'
- return await transporter.sendMail({
- from: from || `"Speckle" <${emailFrom}>`,
- to,
- subject,
- text,
- html
- })
- } catch (error) {
- errorDebug(error)
- }
-}
diff --git a/packages/server/modules/emails/index.ts b/packages/server/modules/emails/index.ts
new file mode 100644
index 000000000..a6beb5319
--- /dev/null
+++ b/packages/server/modules/emails/index.ts
@@ -0,0 +1,44 @@
+/* istanbul ignore file */
+import * as SendingService from '@/modules/emails/services/sending'
+import { initializeTransporter } from '@/modules/emails/utils/transporter'
+import { SpeckleModule } from '@/modules/shared/helpers/typeHelper'
+import dbg from 'debug'
+import { noop } from 'lodash'
+
+const debug = dbg('speckle')
+const modulesDebug = debug.extend('modules')
+
+const emailsModule: SpeckleModule = {
+ init: async (app) => {
+ modulesDebug('📧 Init emails module')
+
+ // init transporter
+ await initializeTransporter()
+
+ // init rest api
+ ;(await import('./rest')).default(app)
+ },
+
+ finalize: noop
+}
+
+async function sendEmail({
+ from,
+ to,
+ subject,
+ text,
+ html
+}: {
+ from?: string
+ to: string
+ subject: string
+ text: string
+ html: string
+}) {
+ return SendingService.sendEmail({ from, to, subject, text, html })
+}
+
+export = {
+ ...emailsModule,
+ sendEmail
+}
diff --git a/packages/server/modules/emails/services/sending.ts b/packages/server/modules/emails/services/sending.ts
new file mode 100644
index 000000000..9cc28cae4
--- /dev/null
+++ b/packages/server/modules/emails/services/sending.ts
@@ -0,0 +1,42 @@
+import { getTransporter } from '@/modules/emails/utils/transporter'
+import dbg from 'debug'
+
+const debug = dbg('speckle')
+const errorDebug = debug.extend('errors')
+
+/**
+ * Send out an e-mail
+ */
+export async function sendEmail({
+ from,
+ to,
+ subject,
+ text,
+ html
+}: {
+ from?: string
+ to: string
+ subject: string
+ text: string
+ html: string
+}): Promise {
+ const transporter = getTransporter()
+ if (!transporter) {
+ errorDebug('No email transport present. Cannot send emails.')
+ return false
+ }
+ try {
+ const emailFrom = process.env.EMAIL_FROM || 'no-reply@speckle.systems'
+ return await transporter.sendMail({
+ from: from || `"Speckle" <${emailFrom}>`,
+ to,
+ subject,
+ text,
+ html
+ })
+ } catch (error) {
+ errorDebug(error)
+ }
+
+ return false
+}
diff --git a/packages/server/modules/emails/services/templateFormatting.ts b/packages/server/modules/emails/services/templateFormatting.ts
new file mode 100644
index 000000000..4c4b5262e
--- /dev/null
+++ b/packages/server/modules/emails/services/templateFormatting.ts
@@ -0,0 +1,52 @@
+import { packageRoot } from '@/bootstrap'
+import path from 'path'
+import ejs from 'ejs'
+
+type MultiTypeEmailBody = {
+ text: string
+ html: string
+}
+
+export type BasicEmailTemplateParams = {
+ html: { bodyStart?: string; bodyEnd?: string }
+ text: { bodyStart?: string; bodyEnd?: string }
+ cta?: {
+ url: string
+ title: string
+ altTitle?: string
+ }
+ server: {
+ name: string
+ url: string
+ company: string
+ contact: string
+ }
+}
+
+function getPathToTemplatesDir(): string {
+ return path.resolve(packageRoot, './assets/emails/templates/')
+}
+
+function buildTemplatePath(name: string, ext: string): string {
+ return path.resolve(getPathToTemplatesDir(), `./${name}/${name}.${ext}`)
+}
+
+/**
+ * Build an e-mail body using the 'basic' template
+ */
+export async function buildBasicTemplateEmail(
+ params: BasicEmailTemplateParams
+): Promise {
+ const textPath = buildTemplatePath('basic', 'txt')
+ const htmlPath = buildTemplatePath('basic', 'html')
+
+ const [text, html] = await Promise.all([
+ ejs.renderFile(textPath, { params }, { cache: true, outputFunctionName: 'print' }),
+ ejs.renderFile(htmlPath, { params }, { cache: true, outputFunctionName: 'print' })
+ ])
+
+ return {
+ text,
+ html
+ }
+}
diff --git a/packages/server/modules/emails/utils/transporter.js b/packages/server/modules/emails/utils/transporter.js
new file mode 100644
index 000000000..b9ec630c6
--- /dev/null
+++ b/packages/server/modules/emails/utils/transporter.js
@@ -0,0 +1,62 @@
+const debug = require('debug')('speckle')
+
+const nodemailer = require('nodemailer')
+const modulesDebug = debug.extend('modules')
+const errorDebug = debug.extend('errors')
+
+/** @type {import('nodemailer').Transporter | undefined} */
+let transporter = undefined
+
+const createJsonEchoTransporter = () =>
+ nodemailer.createTransport({
+ jsonTransport: true
+ })
+
+const initSmtpTransporter = async () => {
+ try {
+ const smtpTransporter = nodemailer.createTransport({
+ host: process.env.EMAIL_HOST,
+ port: process.env.EMAIL_PORT || 587,
+ secure: process.env.EMAIL_SECURE === 'true',
+ auth: {
+ user: process.env.EMAIL_USERNAME,
+ pass: process.env.EMAIL_PASSWORD
+ }
+ })
+ await smtpTransporter.verify()
+ return smtpTransporter
+ } catch {
+ errorDebug('📧 Email provider is misconfigured, check config variables.')
+ }
+}
+
+/**
+ * @returns {import('nodemailer').Transporter | undefined}
+ */
+async function initializeTransporter() {
+ let newTransporter = undefined
+
+ if (process.env.NODE_ENV === 'test') newTransporter = createJsonEchoTransporter()
+ if (process.env.EMAIL === 'true') newTransporter = await initSmtpTransporter()
+
+ if (!newTransporter) {
+ modulesDebug(
+ '📧 Email provider is not configured. Server functionality will be limited.'
+ )
+ }
+
+ transporter = newTransporter
+ return newTransporter
+}
+
+/**
+ * @returns {import('nodemailer').Transporter | undefined}
+ */
+function getTransporter() {
+ return transporter
+}
+
+module.exports = {
+ initializeTransporter,
+ getTransporter
+}
diff --git a/packages/server/modules/index.js b/packages/server/modules/index.js
index 01959cab7..50fef36e5 100644
--- a/packages/server/modules/index.js
+++ b/packages/server/modules/index.js
@@ -1,7 +1,7 @@
'use strict'
const fs = require('fs')
const path = require('path')
-const { appRoot, repoRoot } = require('@/bootstrap')
+const { appRoot, packageRoot } = require('@/bootstrap')
const { values, merge, camelCase } = require('lodash')
const baseTypeDefs = require('@/modules/core/graph/schema/baseTypeDefs')
const { scalarResolvers } = require('./core/graph/scalars')
@@ -61,9 +61,9 @@ exports.graph = () => {
let schemaDirectives = {}
// load typedefs from /assets
- const assetModuleDirs = fs.readdirSync(`${repoRoot}/assets`)
+ const assetModuleDirs = fs.readdirSync(`${packageRoot}/assets`)
assetModuleDirs.forEach((dir) => {
- const typeDefDirPath = path.join(`${repoRoot}/assets`, dir, 'typedefs')
+ const typeDefDirPath = path.join(`${packageRoot}/assets`, dir, 'typedefs')
if (fs.existsSync(typeDefDirPath)) {
const moduleSchemas = fs.readdirSync(typeDefDirPath)
moduleSchemas.forEach((schema) => {
diff --git a/packages/server/modules/serverinvites/repositories/index.js b/packages/server/modules/serverinvites/repositories/index.js
index ab3442c46..337754b35 100644
--- a/packages/server/modules/serverinvites/repositories/index.js
+++ b/packages/server/modules/serverinvites/repositories/index.js
@@ -26,6 +26,7 @@ const { getStream } = require('@/modules/core/repositories/streams')
*/
/**
+ *
* Resolve resource from invite
* @param {import('@/modules/serverinvites/helpers/inviteHelper').InviteResourceData} invite
* @returns {Promise