diff --git a/backend/src/config/index.ts b/backend/src/config/index.ts index f6ea44fa6..c625456f3 100644 --- a/backend/src/config/index.ts +++ b/backend/src/config/index.ts @@ -32,27 +32,6 @@ const environment = { LOG_LEVEL: 'DEBUG', } -const required = { - MAPBOX_TOKEN: env.MAPBOX_TOKEN, - JWT_SECRET: env.JWT_SECRET, - PRIVATE_KEY_PASSPHRASE: env.PRIVATE_KEY_PASSPHRASE, -} - -// https://stackoverflow.com/a/53050575 -type NoUndefinedField = { [P in keyof T]-?: NoUndefinedField> } - -function assertRequiredConfig( - conf: typeof required, -): asserts conf is NoUndefinedField { - Object.entries(conf).forEach(([key, value]) => { - if (!value) { - throw new Error(`ERROR: "${key}" env variable is missing.`) - } - }) -} - -assertRequiredConfig(required) - const server = { CLIENT_URI: env.CLIENT_URI ?? 'http://localhost:3000', GRAPHQL_URI: env.GRAPHQL_URI ?? 'http://localhost:4000', @@ -112,33 +91,34 @@ const redis = { REDIS_PASSWORD: env.REDIS_PASSWORD, } -const s3 = { +const required = { AWS_ACCESS_KEY_ID: env.AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY: env.AWS_SECRET_ACCESS_KEY, AWS_ENDPOINT: env.AWS_ENDPOINT, AWS_REGION: env.AWS_REGION, AWS_BUCKET: env.AWS_BUCKET, - S3_PUBLIC_GATEWAY: env.S3_PUBLIC_GATEWAY, + + MAPBOX_TOKEN: env.MAPBOX_TOKEN, + JWT_SECRET: env.JWT_SECRET, + PRIVATE_KEY_PASSPHRASE: env.PRIVATE_KEY_PASSPHRASE, } -export interface S3Configured { - AWS_ACCESS_KEY_ID: string - AWS_SECRET_ACCESS_KEY: string - AWS_ENDPOINT: string - AWS_REGION: string - AWS_BUCKET: string - S3_PUBLIC_GATEWAY: string | undefined +const S3_PUBLIC_GATEWAY = env.S3_PUBLIC_GATEWAY + +// https://stackoverflow.com/a/53050575 +type NoUndefinedField = { [P in keyof T]-?: NoUndefinedField> } + +function assertRequiredConfig( + conf: typeof required, +): asserts conf is NoUndefinedField { + Object.entries(conf).forEach(([key, value]) => { + if (!value) { + throw new Error(`ERROR: "${key}" env variable is missing.`) + } + }) } -export const isS3configured = (config: typeof s3): config is S3Configured => { - return !!( - config.AWS_ACCESS_KEY_ID && - config.AWS_SECRET_ACCESS_KEY && - config.AWS_ENDPOINT && - config.AWS_REGION && - config.AWS_BUCKET - ) -} +assertRequiredConfig(required) const options = { EMAIL_DEFAULT_SENDER: env.EMAIL_DEFAULT_SENDER, @@ -169,12 +149,21 @@ const CONFIG = { ...neo4j, ...sentry, ...redis, - ...s3, ...options, ...language, + S3_PUBLIC_GATEWAY, } export type Config = typeof CONFIG +export type S3Config = Pick< + Config, + | 'AWS_ACCESS_KEY_ID' + | 'AWS_SECRET_ACCESS_KEY' + | 'AWS_ENDPOINT' + | 'AWS_REGION' + | 'AWS_BUCKET' + | 'S3_PUBLIC_GATEWAY' +> export default CONFIG export { nodemailerTransportOptions } diff --git a/backend/src/graphql/resolvers/attachments/attachments.spec.ts b/backend/src/graphql/resolvers/attachments/attachments.spec.ts index 79b78ad9c..f5ac5ddff 100644 --- a/backend/src/graphql/resolvers/attachments/attachments.spec.ts +++ b/backend/src/graphql/resolvers/attachments/attachments.spec.ts @@ -16,7 +16,7 @@ import { CreateMessage } from '@graphql/queries/CreateMessage' import { createRoomMutation } from '@graphql/queries/createRoomMutation' import type { ApolloTestSetup } from '@root/test/helpers' import { createApolloTestSetup } from '@root/test/helpers' -import type { S3Configured } from '@src/config' +import type { S3Config } from '@src/config' import { attachments } from './attachments' @@ -37,7 +37,7 @@ const UploadMock = { ;(Upload as unknown as jest.Mock).mockImplementation(() => UploadMock) -const config: S3Configured = { +const config: S3Config = { AWS_ACCESS_KEY_ID: 'AWS_ACCESS_KEY_ID', AWS_SECRET_ACCESS_KEY: 'AWS_SECRET_ACCESS_KEY', AWS_BUCKET: 'AWS_BUCKET', diff --git a/backend/src/graphql/resolvers/attachments/attachments.ts b/backend/src/graphql/resolvers/attachments/attachments.ts index ff1d4df59..6a961dfe7 100644 --- a/backend/src/graphql/resolvers/attachments/attachments.ts +++ b/backend/src/graphql/resolvers/attachments/attachments.ts @@ -4,7 +4,7 @@ import { UserInputError } from 'apollo-server-express' import slug from 'slug' import { v4 as uuid } from 'uuid' -import { isS3configured, S3Configured } from '@config/index' +import type { S3Config } from '@config/index' import { wrapTransaction } from '@graphql/resolvers/images/wrapTransaction' import { s3Service } from '@src/uploads/s3Service' @@ -54,11 +54,7 @@ export interface Attachments { ) => Promise } -export const attachments = (config: S3Configured) => { - if (!isS3configured(config)) { - throw new Error('S3 not configured') - } - +export const attachments = (config: S3Config) => { const s3 = s3Service(config, 'attachments') const del: Attachments['del'] = async (resource, relationshipType, opts = {}) => { diff --git a/backend/src/graphql/resolvers/images/images.ts b/backend/src/graphql/resolvers/images/images.ts index dae73bd25..0130f2436 100644 --- a/backend/src/graphql/resolvers/images/images.ts +++ b/backend/src/graphql/resolvers/images/images.ts @@ -1,8 +1,6 @@ -import { isS3configured } from '@config/index' import type { Context } from '@src/context' import type { FileDeleteCallback, FileUploadCallback } from '@src/uploads/types' -import { images as imagesLocal } from './imagesLocal' import { images as imagesS3 } from './imagesS3' import type { FileUpload } from 'graphql-upload' @@ -52,5 +50,4 @@ export interface Images { ) => Promise } -export const images = (config: Context['config']) => - isS3configured(config) ? imagesS3(config) : imagesLocal +export const images = (config: Context['config']) => imagesS3(config) diff --git a/backend/src/graphql/resolvers/images/imagesLocal.spec.ts b/backend/src/graphql/resolvers/images/imagesLocal.spec.ts deleted file mode 100644 index 4fe459699..000000000 --- a/backend/src/graphql/resolvers/images/imagesLocal.spec.ts +++ /dev/null @@ -1,364 +0,0 @@ -/* eslint-disable @typescript-eslint/require-await */ - -/* eslint-disable @typescript-eslint/no-unsafe-return */ -/* eslint-disable @typescript-eslint/restrict-template-expressions */ -/* eslint-disable @typescript-eslint/no-unsafe-call */ -/* eslint-disable @typescript-eslint/no-unsafe-member-access */ -/* eslint-disable @typescript-eslint/no-unsafe-assignment */ -/* eslint-disable promise/prefer-await-to-callbacks */ -import { UserInputError } from 'apollo-server' - -import Factory, { cleanDatabase } from '@db/factories' -import { getNeode, getDriver } from '@db/neo4j' - -import { images } from './imagesLocal' - -import type { ImageInput } from './images' -import type { FileUpload } from 'graphql-upload' - -const driver = getDriver() -const neode = getNeode() -const uuid = '[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}' -let uploadCallback -let deleteCallback - -beforeAll(async () => { - await cleanDatabase() -}) - -afterAll(async () => { - await cleanDatabase() - await driver.close() -}) - -beforeEach(async () => { - uploadCallback = jest.fn(({ uniqueFilename }) => `/uploads/${uniqueFilename}`) - deleteCallback = jest.fn() -}) - -// TODO: avoid database clean after each test in the future if possible for performance and flakyness reasons by filling the database step by step, see issue https://github.com/Ocelot-Social-Community/Ocelot-Social/issues/4543 -afterEach(async () => { - await cleanDatabase() -}) - -describe('deleteImage', () => { - const { deleteImage } = images - - describe('given a resource with an image', () => { - let user: { id: string } - beforeEach(async () => { - const u = await Factory.build( - 'user', - {}, - { - avatar: Factory.build('image', { - url: 'http://localhost/some/avatar/url/', - alt: 'This is the avatar image of a user', - }), - }, - ) - user = await u.toJson() - }) - - it('deletes `Image` node', async () => { - await expect(neode.all('Image')).resolves.toHaveLength(1) - await deleteImage(user, 'AVATAR_IMAGE', { deleteCallback }) - await expect(neode.all('Image')).resolves.toHaveLength(0) - }) - - it('calls deleteCallback', async () => { - const u = await Factory.build('user') - user = await u.toJson() - await deleteImage(user, 'AVATAR_IMAGE', { deleteCallback }) - expect(deleteCallback).toHaveBeenCalled() - }) - - describe('given a transaction parameter', () => { - it('executes cypher statements within the transaction', async () => { - const session = driver.session() - let someString: string - try { - someString = await session.writeTransaction(async (transaction) => { - await deleteImage(user, 'AVATAR_IMAGE', { - deleteCallback, - transaction, - }) - const txResult = await transaction.run('RETURN "Hello" as result') - const [result] = txResult.records.map((record) => record.get('result')) - return result - }) - } finally { - await session.close() - } - await expect(neode.all('Image')).resolves.toHaveLength(0) - expect(someString).toEqual('Hello') - }) - - it('rolls back the transaction in case of errors', async () => { - await expect(neode.all('Image')).resolves.toHaveLength(1) - const session = driver.session() - try { - await session.writeTransaction(async (transaction) => { - await deleteImage(user, 'AVATAR_IMAGE', { - deleteCallback, - transaction, - }) - throw new Error('Ouch!') - }) - // eslint-disable-next-line no-catch-all/no-catch-all - } catch (err) { - // nothing has been deleted - await expect(neode.all('Image')).resolves.toHaveLength(1) - // all good - } finally { - await session.close() - } - }) - }) - }) -}) - -describe('mergeImage', () => { - const { mergeImage } = images - let imageInput: ImageInput - let post: { id: string } - beforeEach(() => { - imageInput = { - alt: 'A description of the new image', - } - }) - - describe('given image.upload', () => { - beforeEach(() => { - const createReadStream: FileUpload['createReadStream'] = (() => ({ - pipe: () => ({ - on: (_, callback) => callback(), - }), - })) as unknown as FileUpload['createReadStream'] - imageInput = { - ...imageInput, - upload: Promise.resolve({ - filename: 'image.jpg', - mimetype: 'image/jpeg', - encoding: '7bit', - createReadStream, - }), - } - }) - - describe('on existing resource', () => { - beforeEach(async () => { - const p = await Factory.build( - 'post', - { id: 'p99' }, - { - author: Factory.build('user', {}, { avatar: null }), - image: null, - }, - ) - post = await p.toJson() - }) - - it('returns new image', async () => { - await expect( - mergeImage(post, 'HERO_IMAGE', imageInput, { uploadCallback, deleteCallback }), - ).resolves.toMatchObject({ - url: expect.any(String), - alt: 'A description of the new image', - }) - }) - - it('calls upload callback', async () => { - await mergeImage(post, 'HERO_IMAGE', imageInput, { uploadCallback, deleteCallback }) - expect(uploadCallback).toHaveBeenCalled() - }) - - it('creates `:Image` node', async () => { - await expect(neode.all('Image')).resolves.toHaveLength(0) - await mergeImage(post, 'HERO_IMAGE', imageInput, { uploadCallback, deleteCallback }) - await expect(neode.all('Image')).resolves.toHaveLength(1) - }) - - it('creates a url safe name', async () => { - if (!imageInput.upload) { - throw new Error('Test imageInput was not setup correctly.') - } - const upload = await imageInput.upload - upload.filename = '/path/to/awkward?/ file-location/?foo- bar-avatar.jpg' - imageInput.upload = Promise.resolve(upload) - await expect( - mergeImage(post, 'HERO_IMAGE', imageInput, { uploadCallback, deleteCallback }), - ).resolves.toMatchObject({ - url: expect.stringMatching(new RegExp(`^/uploads/${uuid}-foo-bar-avatar.jpg`)), - }) - }) - - it('connects resource with image via given image type', async () => { - await mergeImage(post, 'HERO_IMAGE', imageInput, { uploadCallback, deleteCallback }) - const result = await neode.cypher( - `MATCH(p:Post {id: "p99"})-[:HERO_IMAGE]->(i:Image) RETURN i,p`, - {}, - ) - post = neode.hydrateFirst<{ id: string }>(result, 'p', neode.model('Post')).properties() - const image = neode.hydrateFirst(result, 'i', neode.model('Image')) - expect(post).toBeTruthy() - expect(image).toBeTruthy() - }) - - it('sets metadata', async () => { - await mergeImage(post, 'HERO_IMAGE', imageInput, { uploadCallback, deleteCallback }) - const image = await neode.first('Image', {}, undefined) - await expect(image.toJson()).resolves.toMatchObject({ - alt: 'A description of the new image', - createdAt: expect.any(String), - url: expect.any(String), - }) - }) - - describe('given a transaction parameter', () => { - it('executes cypher statements within the transaction', async () => { - const session = driver.session() - try { - await session.writeTransaction(async (transaction) => { - const image = await mergeImage(post, 'HERO_IMAGE', imageInput, { - uploadCallback, - deleteCallback, - transaction, - }) - return transaction.run( - ` - MATCH(image:Image {url: $image.url}) - SET image.alt = 'This alt text gets overwritten' - RETURN image {.*} - `, - { image }, - ) - }) - } finally { - await session.close() - } - const image = await neode.first( - 'Image', - { alt: 'This alt text gets overwritten' }, - undefined, - ) - await expect(image.toJson()).resolves.toMatchObject({ - alt: 'This alt text gets overwritten', - }) - }) - - it('rolls back the transaction in case of errors', async () => { - const session = driver.session() - try { - await session.writeTransaction(async (transaction) => { - const image = await mergeImage(post, 'HERO_IMAGE', imageInput, { - uploadCallback, - deleteCallback, - transaction, - }) - return transaction.run('Ooops invalid cypher!', { image }) - }) - // eslint-disable-next-line no-catch-all/no-catch-all - } catch (err) { - // nothing has been created - await expect(neode.all('Image')).resolves.toHaveLength(0) - // all good - } finally { - await session.close() - } - }) - }) - - describe('if resource has an image already', () => { - beforeEach(async () => { - const [post, image] = await Promise.all([ - neode.find('Post', 'p99'), - Factory.build('image'), - ]) - await post.relateTo(image, 'image') - }) - - it('calls deleteCallback', async () => { - await mergeImage(post, 'HERO_IMAGE', imageInput, { uploadCallback, deleteCallback }) - expect(deleteCallback).toHaveBeenCalled() - }) - - it('calls uploadCallback', async () => { - await mergeImage(post, 'HERO_IMAGE', imageInput, { uploadCallback, deleteCallback }) - expect(uploadCallback).toHaveBeenCalled() - }) - - it('updates metadata of existing image node', async () => { - await expect(neode.all('Image')).resolves.toHaveLength(1) - await mergeImage(post, 'HERO_IMAGE', imageInput, { uploadCallback, deleteCallback }) - await expect(neode.all('Image')).resolves.toHaveLength(1) - const image = await neode.first('Image', {}, undefined) - await expect(image.toJson()).resolves.toMatchObject({ - alt: 'A description of the new image', - createdAt: expect.any(String), - url: expect.any(String), - // TODO - // width: - // height: - }) - }) - }) - }) - }) - - describe('without image.upload', () => { - it('throws UserInputError', async () => { - const p = await Factory.build('post', { id: 'p99' }, { image: null }) - post = await p.toJson() - await expect(mergeImage(post, 'HERO_IMAGE', imageInput)).rejects.toEqual( - new UserInputError('Cannot find image for given resource'), - ) - }) - - describe('if resource has an image already', () => { - beforeEach(async () => { - const p = await Factory.build( - 'post', - { - id: 'p99', - }, - { - author: Factory.build( - 'user', - {}, - { - avatar: null, - }, - ), - image: Factory.build('image', { - alt: 'This is the previous, not updated image', - url: 'http://localhost/some/original/url', - }), - }, - ) - post = await p.toJson() - }) - - it('does not call deleteCallback', async () => { - await mergeImage(post, 'HERO_IMAGE', imageInput, { uploadCallback, deleteCallback }) - expect(deleteCallback).not.toHaveBeenCalled() - }) - - it('does not call uploadCallback', async () => { - await mergeImage(post, 'HERO_IMAGE', imageInput, { uploadCallback, deleteCallback }) - expect(uploadCallback).not.toHaveBeenCalled() - }) - - it('updates metadata', async () => { - await mergeImage(post, 'HERO_IMAGE', imageInput, { uploadCallback, deleteCallback }) - const images = await neode.all('Image') - expect(images).toHaveLength(1) - await expect(images.first().toJson()).resolves.toMatchObject({ - createdAt: expect.any(String), - url: expect.any(String), - alt: 'A description of the new image', - }) - }) - }) - }) -}) diff --git a/backend/src/graphql/resolvers/images/imagesLocal.ts b/backend/src/graphql/resolvers/images/imagesLocal.ts deleted file mode 100644 index e17318840..000000000 --- a/backend/src/graphql/resolvers/images/imagesLocal.ts +++ /dev/null @@ -1,133 +0,0 @@ -/* eslint-disable @typescript-eslint/require-await */ - -/* eslint-disable @typescript-eslint/no-unsafe-call */ -/* eslint-disable @typescript-eslint/no-unsafe-member-access */ -/* eslint-disable @typescript-eslint/no-unsafe-return */ -/* eslint-disable @typescript-eslint/no-unsafe-assignment */ -/* eslint-disable @typescript-eslint/no-unsafe-argument */ -/* eslint-disable promise/avoid-new */ -/* eslint-disable security/detect-non-literal-fs-filename */ - -import { existsSync, unlinkSync, createWriteStream } from 'node:fs' -import path from 'node:path' - -import { UserInputError } from 'apollo-server' -import slug from 'slug' -import { v4 as uuid } from 'uuid' - -import type { FileDeleteCallback, FileUploadCallback } from '@src/uploads/types' - -import { wrapTransaction } from './wrapTransaction' - -import type { Images } from './images' -import type { FileUpload } from 'graphql-upload' - -const deleteImage: Images['deleteImage'] = async (resource, relationshipType, opts = {}) => { - const { transaction, deleteCallback } = opts - if (!transaction) return wrapTransaction(deleteImage, [resource, relationshipType], opts) - const txResult = await transaction.run( - ` - MATCH (resource {id: $resource.id})-[rel:${relationshipType}]->(image:Image) - WITH image, image {.*} as imageProps - DETACH DELETE image - RETURN imageProps - `, - { resource }, - ) - const [image] = txResult.records.map((record) => record.get('imageProps')) - // This behaviour differs from `mergeImage`. If you call `mergeImage` - // with metadata for an image that does not exist, it's an indicator - // of an error (so throw an error). If we bulk delete an image, it - // could very well be that there is no image for the resource. - if (image) deleteImageFile(image, deleteCallback) - return image -} - -const mergeImage: Images['mergeImage'] = async ( - resource, - relationshipType, - imageInput, - opts = {}, -) => { - if (typeof imageInput === 'undefined') return - if (imageInput === null) return deleteImage(resource, relationshipType, opts) - const { transaction, uploadCallback, deleteCallback } = opts - if (!transaction) - return wrapTransaction(mergeImage, [resource, relationshipType, imageInput], opts) - - let txResult - txResult = await transaction.run( - ` - MATCH (resource {id: $resource.id})-[:${relationshipType}]->(image:Image) - RETURN image {.*} - `, - { resource }, - ) - const [existingImage] = txResult.records.map((record) => record.get('image')) - const { upload } = imageInput - if (!(existingImage || upload)) throw new UserInputError('Cannot find image for given resource') - if (existingImage && upload) deleteImageFile(existingImage, deleteCallback) - const url = await uploadImageFile(upload, uploadCallback) - const { alt, sensitive, aspectRatio, type } = imageInput - const image = { alt, sensitive, aspectRatio, url, type } - txResult = await transaction.run( - ` - MATCH (resource {id: $resource.id}) - MERGE (resource)-[:${relationshipType}]->(image:Image) - ON CREATE SET image.createdAt = toString(datetime()) - ON MATCH SET image.updatedAt = toString(datetime()) - SET image += $image - RETURN image {.*} - `, - { resource, image }, - ) - const [mergedImage] = txResult.records.map((record) => record.get('image')) - return mergedImage -} - -const localFileDelete: FileDeleteCallback = async (url) => { - const location = `public${url}` - // eslint-disable-next-line n/no-sync - if (existsSync(location)) unlinkSync(location) -} - -const deleteImageFile = (image, deleteCallback: FileDeleteCallback | undefined) => { - if (!deleteCallback) { - deleteCallback = localFileDelete - } - const { url } = image - // eslint-disable-next-line @typescript-eslint/no-floating-promises - deleteCallback(url) - return url -} - -const uploadImageFile = async ( - upload: Promise | undefined, - uploadCallback: FileUploadCallback | undefined, -) => { - if (!upload) return undefined - if (!uploadCallback) { - uploadCallback = localFileUpload - } - // eslint-disable-next-line @typescript-eslint/unbound-method - const { createReadStream, filename, mimetype } = await upload - const { name, ext } = path.parse(filename) - const uniqueFilename = `${uuid()}-${slug(name)}${ext}` - return uploadCallback({ createReadStream, uniqueFilename, mimetype }) -} - -const localFileUpload: FileUploadCallback = ({ createReadStream, uniqueFilename }) => { - const destination = `/uploads/${uniqueFilename}` - return new Promise((resolve, reject) => - createReadStream().pipe( - createWriteStream(`public${destination}`) - .on('finish', () => resolve(destination)) - .on('error', (error) => reject(error)), - ), - ) -} - -export const images: Images = { - deleteImage, - mergeImage, -} diff --git a/backend/src/graphql/resolvers/images/imagesS3.spec.ts b/backend/src/graphql/resolvers/images/imagesS3.spec.ts index b6080d757..0fe885f7c 100644 --- a/backend/src/graphql/resolvers/images/imagesS3.spec.ts +++ b/backend/src/graphql/resolvers/images/imagesS3.spec.ts @@ -9,7 +9,7 @@ import { UserInputError } from 'apollo-server' import Factory, { cleanDatabase } from '@db/factories' import { getNeode, getDriver } from '@db/neo4j' -import type { S3Configured } from '@src/config' +import type { S3Config } from '@src/config' import { images } from './imagesS3' @@ -41,7 +41,7 @@ const driver = getDriver() const neode = getNeode() const uuid = '[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}' -const config: S3Configured = { +const config: S3Config = { AWS_ACCESS_KEY_ID: 'AWS_ACCESS_KEY_ID', AWS_SECRET_ACCESS_KEY: 'AWS_SECRET_ACCESS_KEY', AWS_BUCKET: 'AWS_BUCKET', diff --git a/backend/src/graphql/resolvers/images/imagesS3.ts b/backend/src/graphql/resolvers/images/imagesS3.ts index 7778e9de9..6c154f89a 100644 --- a/backend/src/graphql/resolvers/images/imagesS3.ts +++ b/backend/src/graphql/resolvers/images/imagesS3.ts @@ -5,14 +5,14 @@ import { FileUpload } from 'graphql-upload' import slug from 'slug' import { v4 as uuid } from 'uuid' -import type { S3Configured } from '@config/index' +import type { S3Config } from '@config/index' import { s3Service } from '@src/uploads/s3Service' import { wrapTransaction } from './wrapTransaction' import type { Image, Images } from './images' -export const images = (config: S3Configured) => { +export const images = (config: S3Config) => { const s3 = s3Service(config, 'original') const deleteImage: Images['deleteImage'] = async (resource, relationshipType, opts = {}) => { @@ -89,9 +89,9 @@ export const images = (config: S3Configured) => { return await s3.uploadFile({ ...upload, uniqueFilename }) } - const images: Images = { + const images = { deleteImage, mergeImage, - } + } satisfies Images return images } diff --git a/backend/src/graphql/resolvers/messages.ts b/backend/src/graphql/resolvers/messages.ts index 1c588ba5d..ad75dd181 100644 --- a/backend/src/graphql/resolvers/messages.ts +++ b/backend/src/graphql/resolvers/messages.ts @@ -7,7 +7,7 @@ import { withFilter } from 'graphql-subscriptions' import { neo4jgraphql } from 'neo4j-graphql-js' -import CONFIG, { isS3configured } from '@config/index' +import CONFIG from '@config/index' import { CHAT_MESSAGE_ADDED } from '@constants/subscriptions' import { attachments } from './attachments/attachments' @@ -125,19 +125,17 @@ export default { const atns: File[] = [] - if (isS3configured(CONFIG)) { - for await (const file of files) { - const atn = await attachments(CONFIG).add( - message, - 'ATTACHMENT', - file, - {}, - { - transaction, - }, - ) - atns.push(atn) - } + for await (const file of files) { + const atn = await attachments(CONFIG).add( + message, + 'ATTACHMENT', + file, + {}, + { + transaction, + }, + ) + atns.push(atn) } return { ...message, files: atns } diff --git a/backend/src/uploads/s3Service.ts b/backend/src/uploads/s3Service.ts index e69d13c84..816d9fe4a 100644 --- a/backend/src/uploads/s3Service.ts +++ b/backend/src/uploads/s3Service.ts @@ -1,11 +1,11 @@ import { S3Client, DeleteObjectCommand, ObjectCannedACL } from '@aws-sdk/client-s3' import { Upload } from '@aws-sdk/lib-storage' -import type { S3Configured } from '@config/index' +import type { S3Config } from '@config/index' import { FileUploadCallback, FileDeleteCallback } from './types' -export const s3Service = (config: S3Configured, prefix: string) => { +export const s3Service = (config: S3Config, prefix: string) => { const { AWS_BUCKET: Bucket } = config const { AWS_ENDPOINT, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, S3_PUBLIC_GATEWAY } = config diff --git a/backend/test/helpers.ts b/backend/test/helpers.ts index 75a20fc76..c75dd47d5 100644 --- a/backend/test/helpers.ts +++ b/backend/test/helpers.ts @@ -37,11 +37,12 @@ export const TEST_CONFIG = { REDIS_PORT: undefined, REDIS_PASSWORD: undefined, - AWS_ACCESS_KEY_ID: '', - AWS_SECRET_ACCESS_KEY: '', - AWS_ENDPOINT: '', - AWS_REGION: '', - AWS_BUCKET: '', + AWS_ACCESS_KEY_ID: 'minio', + AWS_SECRET_ACCESS_KEY: '12341234', + AWS_ENDPOINT: 'http:/minio:9000', + AWS_REGION: 'local', + AWS_BUCKET: 'ocelot', + S3_PUBLIC_GATEWAY: undefined, EMAIL_DEFAULT_SENDER: '',